@@ -13,9 +13,13 @@ import { Https } from '../utils/https';
1313import { installDependencies } from '../utils/install-dependencies' ;
1414import { getAvailableLocales } from '../utils/localization' ;
1515import { login , storeCredentials } from '../utils/login' ;
16+ import { promptForInfrastructureProject } from '../utils/prompt-infrastructure-project' ;
17+ import { setupNativeHosting } from '../utils/setup-native-hosting' ;
1618import { Telemetry } from '../utils/telemetry/telemetry' ;
1719import { writeEnv } from '../utils/write-env' ;
1820
21+ type HostingMode = 'self-hosted' | 'commerce' ;
22+
1923interface 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+
286408function 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
0 commit comments