Skip to content

Commit b6fb183

Browse files
committed
TRAC-137: Add native hosting option in create-catalyst
1 parent ed76224 commit b6fb183

10 files changed

Lines changed: 1222 additions & 1 deletion

packages/create-catalyst/src/commands/create.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ import { Https } from '../utils/https';
1313
import { installDependencies } from '../utils/install-dependencies';
1414
import { getAvailableLocales } from '../utils/localization';
1515
import { login, storeCredentials } from '../utils/login';
16+
import { promptForInfrastructureProject } from '../utils/prompt-infrastructure-project';
17+
import { setupNativeHosting } from '../utils/setup-native-hosting';
1618
import { Telemetry } from '../utils/telemetry/telemetry';
1719
import { writeEnv } from '../utils/write-env';
1820

21+
type HostingMode = 'self-hosted' | 'commerce';
22+
1923
interface Channel {
2024
id: number;
2125
name: string;
@@ -283,6 +287,124 @@ async function setupProject(options: {
283287
return { projectName, projectDir };
284288
}
285289

290+
type HostingResolution =
291+
| { mode: 'self-hosted' }
292+
| { mode: 'commerce'; projectUuid: string; accessToken: string };
293+
294+
async function resolveProjectsAccess(
295+
cliApi: CliApi,
296+
hostingFlag: HostingMode | undefined,
297+
): Promise<boolean | null> {
298+
try {
299+
return await cliApi.hasProjectsAccess();
300+
} catch (error) {
301+
const message = error instanceof Error ? error.message : 'unknown error';
302+
303+
if (hostingFlag === 'commerce') {
304+
console.error(
305+
colorize(
306+
'red',
307+
`\nFailed to verify Infrastructure Projects API access: ${message}\nPlease try again.\n`,
308+
),
309+
);
310+
process.exit(1);
311+
}
312+
313+
console.warn(
314+
colorize(
315+
'yellow',
316+
`\nCould not verify Infrastructure Projects API access: ${message}\nDefaulting to self-hosted (Next.js). Re-run create-catalyst if you intended to use Commerce-hosted.\n`,
317+
),
318+
);
319+
320+
return null;
321+
}
322+
}
323+
324+
async function resolveHostingMode({
325+
hostingFlag,
326+
storeHash,
327+
accessToken,
328+
cliApiOrigin,
329+
apiHostname,
330+
defaultProjectName,
331+
}: {
332+
hostingFlag?: HostingMode;
333+
storeHash?: string;
334+
accessToken?: string;
335+
cliApiOrigin: string;
336+
apiHostname: string;
337+
defaultProjectName?: string;
338+
}): Promise<HostingResolution> {
339+
if (hostingFlag === 'self-hosted') {
340+
return { mode: 'self-hosted' };
341+
}
342+
343+
if (!storeHash || !accessToken) {
344+
if (hostingFlag === 'commerce') {
345+
console.error(
346+
colorize(
347+
'red',
348+
'\n--hosting commerce requires store credentials (store hash and access token)\n',
349+
),
350+
);
351+
process.exit(1);
352+
}
353+
354+
return { mode: 'self-hosted' };
355+
}
356+
357+
const cliApi = new CliApi({
358+
origin: cliApiOrigin,
359+
storeHash,
360+
accessToken,
361+
apiHostname,
362+
});
363+
364+
const hasAccess = await resolveProjectsAccess(cliApi, hostingFlag);
365+
366+
if (hasAccess === null) {
367+
return { mode: 'self-hosted' };
368+
}
369+
370+
if (hostingFlag === 'commerce' && !hasAccess) {
371+
console.error(
372+
colorize(
373+
'red',
374+
'\nThis store does not have access to the Infrastructure Projects API. Contact support@bigcommerce.com to enable it.\n',
375+
),
376+
);
377+
process.exit(1);
378+
}
379+
380+
const useNativeHosting =
381+
hostingFlag === undefined && hasAccess
382+
? await select({
383+
message: 'How would you like to host your Catalyst storefront?',
384+
choices: [
385+
{
386+
name: 'Self-hosted (Next.js)',
387+
value: false,
388+
description: 'Use standard next dev / build / start',
389+
},
390+
{
391+
name: 'Commerce-hosted (Native)',
392+
value: true,
393+
description: 'Use catalyst dev / build / deploy',
394+
},
395+
],
396+
})
397+
: false;
398+
399+
if (!useNativeHosting) {
400+
return { mode: 'self-hosted' };
401+
}
402+
403+
const project = await promptForInfrastructureProject(cliApi, defaultProjectName);
404+
405+
return { mode: 'commerce', projectUuid: project.uuid, accessToken };
406+
}
407+
286408
function checkRequiredTools() {
287409
try {
288410
execSync(getPlatformCheckCommand('git'), { stdio: 'ignore' });
@@ -316,6 +438,12 @@ export const create = new Command('create')
316438
.option('--reset-main', 'Reset the main branch to the gh-ref')
317439
.option('--repository <repository>', 'GitHub repository to clone from', 'bigcommerce/catalyst')
318440
.option('--env <vars...>', 'Arbitrary environment variables to set in .env.local')
441+
.addOption(
442+
new Option(
443+
'--hosting <mode>',
444+
'Hosting mode: "self-hosted" for Next.js or "commerce" for Commerce-hosted',
445+
).choices(['self-hosted', 'commerce'] as const),
446+
)
319447
.addOption(
320448
new Option('--bigcommerce-hostname <hostname>', 'BigCommerce hostname')
321449
.default('bigcommerce.com')
@@ -363,6 +491,16 @@ export const create = new Command('create')
363491
envVars.BIGCOMMERCE_STOREFRONT_API_TOKEN = storefrontToken;
364492
} else {
365493
if (!storeHash || !accessToken) {
494+
if (options.hosting === 'commerce') {
495+
console.error(
496+
colorize(
497+
'red',
498+
'\n--hosting commerce requires store credentials (store hash and access token)\n',
499+
),
500+
);
501+
process.exit(1);
502+
}
503+
366504
// Create project without credentials
367505
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`);
368506
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
@@ -418,6 +556,7 @@ export const create = new Command('create')
418556
origin: options.cliApiOrigin,
419557
storeHash,
420558
accessToken,
559+
apiHostname: `api.${options.bigcommerceHostname}`,
421560
});
422561

423562
// If we have channelId but no storefrontToken, just get the init data
@@ -523,9 +662,32 @@ export const create = new Command('create')
523662
if (!channelId) throw new Error('Something went wrong, channelId is not defined');
524663
if (!storefrontToken) throw new Error('Something went wrong, storefrontToken is not defined');
525664

665+
const hosting = await resolveHostingMode({
666+
hostingFlag: options.hosting,
667+
storeHash,
668+
accessToken,
669+
cliApiOrigin: options.cliApiOrigin,
670+
apiHostname: `api.${options.bigcommerceHostname}`,
671+
defaultProjectName: projectName,
672+
});
673+
674+
if (hosting.mode === 'commerce') {
675+
envVars.BIGCOMMERCE_ACCESS_TOKEN = hosting.accessToken;
676+
}
677+
526678
// Create the project with all necessary configuration
527679
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`);
528680
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
681+
682+
if (hosting.mode === 'commerce') {
683+
setupNativeHosting({
684+
projectDir,
685+
projectUuid: hosting.projectUuid,
686+
storeHash,
687+
accessToken: hosting.accessToken,
688+
});
689+
}
690+
529691
await installDependencies(projectDir);
530692

531693
// Write env vars
@@ -540,6 +702,14 @@ export const create = new Command('create')
540702
colorize('green', `\nSuccess! Created '${projectName}' at '${projectDir}'\n`),
541703
'\nNext steps:\n',
542704
colorize('yellow', `\ncd ${projectName} && pnpm run dev\n`),
705+
...(hosting.mode === 'commerce'
706+
? [
707+
colorize(
708+
'yellow',
709+
`\nRun 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce hosting.\n`,
710+
),
711+
]
712+
: []),
543713
);
544714
});
545715

packages/create-catalyst/src/commands/init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const init = new Command('init')
109109
origin: options.cliApiOrigin,
110110
storeHash,
111111
accessToken,
112+
apiHostname: `api.${options.bigcommerceHostname}`,
112113
});
113114

114115
const channelSortOrder = ['catalyst', 'next', 'bigcommerce'];

packages/create-catalyst/src/utils/auth.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export class Auth {
2828
'store_v2_information',
2929
'store_v2_products',
3030
'store_cart',
31+
'store_infrastructure_projects_manage',
32+
'store_infrastructure_deployments_manage',
33+
'store_infrastructure_logs_read_only',
3134
].join(' '),
3235
client_id: this.DEVICE_OAUTH_CLIENT_ID,
3336
}),
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class InfrastructureProjectValidationError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = 'InfrastructureProjectValidationError';
5+
}
6+
}

0 commit comments

Comments
 (0)