Skip to content

Commit 119cc17

Browse files
committed
LTRAC-137: Add native hosting option in create-catalyst
1 parent ed76224 commit 119cc17

10 files changed

Lines changed: 1214 additions & 1 deletion

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

Lines changed: 162 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 = 'next' | 'catalyst';
22+
1923
interface Channel {
2024
id: number;
2125
name: string;
@@ -283,6 +287,116 @@ async function setupProject(options: {
283287
return { projectName, projectDir };
284288
}
285289

290+
type HostingResolution =
291+
| { mode: 'next' }
292+
| { mode: 'catalyst'; projectUuid: string; accessToken: string };
293+
294+
async function resolveHostingMode({
295+
hostingFlag,
296+
storeHash,
297+
accessToken,
298+
cliApiOrigin,
299+
apiHostname,
300+
defaultProjectName,
301+
}: {
302+
hostingFlag?: HostingMode;
303+
storeHash?: string;
304+
accessToken?: string;
305+
cliApiOrigin: string;
306+
apiHostname: string;
307+
defaultProjectName?: string;
308+
}): Promise<HostingResolution> {
309+
if (hostingFlag === 'next') {
310+
return { mode: 'next' };
311+
}
312+
313+
if (!storeHash || !accessToken) {
314+
if (hostingFlag === 'catalyst') {
315+
console.error(
316+
colorize(
317+
'red',
318+
'\n--hosting catalyst requires store credentials (store hash and access token)\n',
319+
),
320+
);
321+
process.exit(1);
322+
}
323+
324+
return { mode: 'next' };
325+
}
326+
327+
const cliApi = new CliApi({
328+
origin: cliApiOrigin,
329+
storeHash,
330+
accessToken,
331+
apiHostname,
332+
});
333+
334+
let hasAccess = false;
335+
336+
try {
337+
hasAccess = await cliApi.checkProjectsAccess();
338+
} catch (error) {
339+
const message = error instanceof Error ? error.message : 'unknown error';
340+
341+
if (hostingFlag === 'catalyst') {
342+
console.error(
343+
colorize(
344+
'red',
345+
`\nFailed to verify Infrastructure Projects API access: ${message}\nPlease try again.\n`,
346+
),
347+
);
348+
process.exit(1);
349+
}
350+
351+
console.warn(
352+
colorize(
353+
'yellow',
354+
`\nCould not verify Infrastructure Projects API access: ${message}\nDefaulting to self-hosted (Next.js). Re-run create-catalyst if you intended to use BigCommerce-hosted.\n`,
355+
),
356+
);
357+
358+
return { mode: 'next' };
359+
}
360+
361+
if (hostingFlag === 'catalyst' && !hasAccess) {
362+
console.error(
363+
colorize(
364+
'red',
365+
'\nThis store does not have access to the Infrastructure Projects API. Contact support@bigcommerce.com to enable it.\n',
366+
),
367+
);
368+
process.exit(1);
369+
}
370+
371+
let useNative = hostingFlag === 'catalyst';
372+
373+
if (hostingFlag === undefined && hasAccess) {
374+
useNative = await select({
375+
message: 'How would you like to host your Catalyst storefront?',
376+
choices: [
377+
{
378+
name: 'Self-hosted (Next.js)',
379+
value: false,
380+
description: 'Use standard next dev / build / start',
381+
},
382+
{
383+
name: 'BigCommerce-hosted (Native)',
384+
value: true,
385+
description: 'Use catalyst dev / build / deploy',
386+
},
387+
],
388+
});
389+
}
390+
391+
if (!useNative) {
392+
return { mode: 'next' };
393+
}
394+
395+
const project = await promptForInfrastructureProject(cliApi, defaultProjectName);
396+
397+
return { mode: 'catalyst', projectUuid: project.uuid, accessToken };
398+
}
399+
286400
function checkRequiredTools() {
287401
try {
288402
execSync(getPlatformCheckCommand('git'), { stdio: 'ignore' });
@@ -316,6 +430,12 @@ export const create = new Command('create')
316430
.option('--reset-main', 'Reset the main branch to the gh-ref')
317431
.option('--repository <repository>', 'GitHub repository to clone from', 'bigcommerce/catalyst')
318432
.option('--env <vars...>', 'Arbitrary environment variables to set in .env.local')
433+
.addOption(
434+
new Option(
435+
'--hosting <mode>',
436+
'Hosting mode: "next" for self-hosted Next.js or "catalyst" for BigCommerce-hosted',
437+
).choices(['next', 'catalyst'] as const),
438+
)
319439
.addOption(
320440
new Option('--bigcommerce-hostname <hostname>', 'BigCommerce hostname')
321441
.default('bigcommerce.com')
@@ -363,6 +483,16 @@ export const create = new Command('create')
363483
envVars.BIGCOMMERCE_STOREFRONT_API_TOKEN = storefrontToken;
364484
} else {
365485
if (!storeHash || !accessToken) {
486+
if (options.hosting === 'catalyst') {
487+
console.error(
488+
colorize(
489+
'red',
490+
'\n--hosting catalyst requires store credentials (store hash and access token)\n',
491+
),
492+
);
493+
process.exit(1);
494+
}
495+
366496
// Create project without credentials
367497
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`);
368498
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
@@ -418,6 +548,7 @@ export const create = new Command('create')
418548
origin: options.cliApiOrigin,
419549
storeHash,
420550
accessToken,
551+
apiHostname: `api.${options.bigcommerceHostname}`,
421552
});
422553

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

657+
const hosting = await resolveHostingMode({
658+
hostingFlag: options.hosting,
659+
storeHash,
660+
accessToken,
661+
cliApiOrigin: options.cliApiOrigin,
662+
apiHostname: `api.${options.bigcommerceHostname}`,
663+
defaultProjectName: projectName,
664+
});
665+
666+
if (hosting.mode === 'catalyst') {
667+
envVars.BIGCOMMERCE_ACCESS_TOKEN = hosting.accessToken;
668+
}
669+
526670
// Create the project with all necessary configuration
527671
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`);
528672
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
673+
674+
if (hosting.mode === 'catalyst') {
675+
setupNativeHosting({
676+
projectDir,
677+
projectUuid: hosting.projectUuid,
678+
storeHash,
679+
accessToken: hosting.accessToken,
680+
});
681+
}
682+
529683
await installDependencies(projectDir);
530684

531685
// Write env vars
@@ -540,6 +694,14 @@ export const create = new Command('create')
540694
colorize('green', `\nSuccess! Created '${projectName}' at '${projectDir}'\n`),
541695
'\nNext steps:\n',
542696
colorize('yellow', `\ncd ${projectName} && pnpm run dev\n`),
697+
...(hosting.mode === 'catalyst'
698+
? [
699+
colorize(
700+
'yellow',
701+
`\nRun 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to BigCommerce hosting.\n`,
702+
),
703+
]
704+
: []),
543705
);
544706
});
545707

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)