Skip to content

Commit dd7aee1

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

10 files changed

Lines changed: 1335 additions & 2 deletions

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

Lines changed: 178 additions & 1 deletion
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;
@@ -271,7 +275,7 @@ async function setupProject(options: {
271275
};
272276

273277
await input({
274-
message: 'What is the name of your project?',
278+
message: 'What do you want to name your project directory?',
275279
default: 'my-catalyst-app',
276280
validate: validateProjectName,
277281
});
@@ -283,6 +287,130 @@ 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+
autoUseProjectName,
332+
}: {
333+
hostingFlag?: HostingMode;
334+
storeHash?: string;
335+
accessToken?: string;
336+
cliApiOrigin: string;
337+
apiHostname: string;
338+
defaultProjectName?: string;
339+
autoUseProjectName?: boolean;
340+
}): Promise<HostingResolution> {
341+
if (hostingFlag === 'self-hosted') {
342+
return { mode: 'self-hosted' };
343+
}
344+
345+
if (!storeHash || !accessToken) {
346+
if (hostingFlag === 'commerce') {
347+
console.error(
348+
colorize(
349+
'red',
350+
'\n--hosting commerce requires store credentials (store hash and access token)\n',
351+
),
352+
);
353+
process.exit(1);
354+
}
355+
356+
return { mode: 'self-hosted' };
357+
}
358+
359+
const cliApi = new CliApi({
360+
origin: cliApiOrigin,
361+
storeHash,
362+
accessToken,
363+
apiHostname,
364+
});
365+
366+
const hasAccess = await resolveProjectsAccess(cliApi, hostingFlag);
367+
368+
if (hasAccess === null) {
369+
return { mode: 'self-hosted' };
370+
}
371+
372+
if (hostingFlag === 'commerce' && !hasAccess) {
373+
console.error(
374+
colorize(
375+
'red',
376+
'\nThis store does not have access to the Infrastructure Projects API. Contact support@bigcommerce.com to enable it.\n',
377+
),
378+
);
379+
process.exit(1);
380+
}
381+
382+
const useNativeHosting =
383+
hostingFlag === undefined && hasAccess
384+
? await select({
385+
message: 'How would you like to host your Catalyst storefront?',
386+
choices: [
387+
{
388+
name: 'Self-hosted (Next.js)',
389+
value: false,
390+
description: 'Use standard next dev / build / start',
391+
},
392+
{
393+
name: 'Commerce-hosted (Native)',
394+
value: true,
395+
description: 'Use catalyst dev / build / deploy',
396+
},
397+
],
398+
})
399+
: false;
400+
401+
if (!useNativeHosting) {
402+
return { mode: 'self-hosted' };
403+
}
404+
405+
const project = await promptForInfrastructureProject(
406+
cliApi,
407+
defaultProjectName,
408+
autoUseProjectName,
409+
);
410+
411+
return { mode: 'commerce', projectUuid: project.uuid, accessToken };
412+
}
413+
286414
function checkRequiredTools() {
287415
try {
288416
execSync(getPlatformCheckCommand('git'), { stdio: 'ignore' });
@@ -316,6 +444,12 @@ export const create = new Command('create')
316444
.option('--reset-main', 'Reset the main branch to the gh-ref')
317445
.option('--repository <repository>', 'GitHub repository to clone from', 'bigcommerce/catalyst')
318446
.option('--env <vars...>', 'Arbitrary environment variables to set in .env.local')
447+
.addOption(
448+
new Option(
449+
'--hosting <mode>',
450+
'Hosting mode: "self-hosted" or "commerce" for Commerce Hosting.',
451+
).choices(['self-hosted', 'commerce'] as const),
452+
)
319453
.addOption(
320454
new Option('--bigcommerce-hostname <hostname>', 'BigCommerce hostname')
321455
.default('bigcommerce.com')
@@ -363,6 +497,16 @@ export const create = new Command('create')
363497
envVars.BIGCOMMERCE_STOREFRONT_API_TOKEN = storefrontToken;
364498
} else {
365499
if (!storeHash || !accessToken) {
500+
if (options.hosting === 'commerce') {
501+
console.error(
502+
colorize(
503+
'red',
504+
'\n--hosting commerce requires store credentials (store hash and access token)\n',
505+
),
506+
);
507+
process.exit(1);
508+
}
509+
366510
// Create project without credentials
367511
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`);
368512
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
@@ -418,6 +562,7 @@ export const create = new Command('create')
418562
origin: options.cliApiOrigin,
419563
storeHash,
420564
accessToken,
565+
apiHostname: `api.${options.bigcommerceHostname}`,
421566
});
422567

423568
// If we have channelId but no storefrontToken, just get the init data
@@ -523,9 +668,33 @@ export const create = new Command('create')
523668
if (!channelId) throw new Error('Something went wrong, channelId is not defined');
524669
if (!storefrontToken) throw new Error('Something went wrong, storefrontToken is not defined');
525670

671+
const hosting = await resolveHostingMode({
672+
hostingFlag: options.hosting,
673+
storeHash,
674+
accessToken,
675+
cliApiOrigin: options.cliApiOrigin,
676+
apiHostname: `api.${options.bigcommerceHostname}`,
677+
defaultProjectName: projectName,
678+
autoUseProjectName: !!options.projectName,
679+
});
680+
681+
if (hosting.mode === 'commerce') {
682+
envVars.BIGCOMMERCE_ACCESS_TOKEN = hosting.accessToken;
683+
}
684+
526685
// Create the project with all necessary configuration
527686
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`);
528687
cloneCatalyst({ repository, projectName, projectDir, ghRef, resetMain: options.resetMain });
688+
689+
if (hosting.mode === 'commerce') {
690+
setupNativeHosting({
691+
projectDir,
692+
projectUuid: hosting.projectUuid,
693+
storeHash,
694+
accessToken: hosting.accessToken,
695+
});
696+
}
697+
529698
await installDependencies(projectDir);
530699

531700
// Write env vars
@@ -540,6 +709,14 @@ export const create = new Command('create')
540709
colorize('green', `\nSuccess! Created '${projectName}' at '${projectDir}'\n`),
541710
'\nNext steps:\n',
542711
colorize('yellow', `\ncd ${projectName} && pnpm run dev\n`),
712+
...(hosting.mode === 'commerce'
713+
? [
714+
colorize(
715+
'yellow',
716+
`\nRun 'cd ${projectName}/core && pnpm run deploy' when ready to deploy to Commerce hosting.\n`,
717+
),
718+
]
719+
: []),
543720
);
544721
});
545722

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)