diff --git a/packages/create-catalyst/src/commands/create.ts b/packages/create-catalyst/src/commands/create.ts index 1e3b52305..b68883547 100644 --- a/packages/create-catalyst/src/commands/create.ts +++ b/packages/create-catalyst/src/commands/create.ts @@ -1,3 +1,4 @@ +import { Command, Option } from '@commander-js/extra-typings'; import { input, select } from '@inquirer/prompts'; import chalk from 'chalk'; import { exec as execCallback } from 'child_process'; @@ -7,216 +8,258 @@ import { join } from 'path'; import { promisify } from 'util'; import { z } from 'zod'; -import { type CreateCommandOptions } from '..'; import { checkStorefrontLimit } from '../utils/check-storefront-limit'; import { cloneCatalyst } from '../utils/clone-catalyst'; +import { getLatestCoreTag } from '../utils/get-latest-core-tag'; import { Https } from '../utils/https'; import { installDependencies } from '../utils/install-dependencies'; import { login } from '../utils/login'; import { parse } from '../utils/parse'; +import { getPackageManager, packageManagerChoices } from '../utils/pm'; import { spinner } from '../utils/spinner'; import { writeEnv } from '../utils/write-env'; const exec = promisify(execCallback); -export const create = async (options: CreateCommandOptions) => { - const { packageManager, codeEditor, includeFunctionalTests } = options; - - const URLSchema = z.string().url(); - const sampleDataApiUrl = parse(options.sampleDataApiUrl, URLSchema); - const bigcommerceApiUrl = parse(`https://api.${options.bigcommerceHostname}`, URLSchema); - const bigcommerceAuthUrl = parse(`https://login.${options.bigcommerceHostname}`, URLSchema); - - let ghRef: string; - - if (options.ghRef instanceof Function) { - ghRef = await options.ghRef(); - } else { - ghRef = options.ghRef; - } - - let projectName; - let projectDir; - let storeHash = options.storeHash; - let accessToken = options.accessToken; - let channelId; - let customerImpersonationToken = options.customerImpersonationToken; - - if (options.channelId) { - channelId = parseInt(options.channelId, 10); - } +export const create = new Command('create') + .description('Command to scaffold and connect a Catalyst storefront to your BigCommerce store') + .option('--project-name ', 'Name of your Catalyst project') + .option('--project-dir ', 'Directory in which to create your project', process.cwd()) + .option('--store-hash ', 'BigCommerce store hash') + .option('--access-token ', 'BigCommerce access token') + .option('--channel-id ', 'BigCommerce channel ID') + .option('--customer-impersonation-token ', 'BigCommerce customer impersonation token') + .addOption( + new Option( + '--gh-ref ', + 'Clone a specific ref from the bigcommerce/catalyst repository', + ).default(getLatestCoreTag), + ) + .addOption( + new Option('--bigcommerce-hostname ', 'BigCommerce hostname') + .default('bigcommerce.com') + .hideHelp(), + ) + .addOption( + new Option('--sample-data-api-url ', 'BigCommerce sample data API URL') + .default('https://api.bc-sample.store') + .hideHelp(), + ) + .addOption( + new Option('--package-manager ', 'Override detected package manager') + .choices(packageManagerChoices) + .default(getPackageManager()) + .hideHelp(), + ) + .addOption( + new Option('--code-editor ', 'Your preferred code editor') + .choices(['vscode']) + .default('vscode') + .hideHelp(), + ) + .addOption( + new Option('--include-functional-tests', 'Include the functional test suite') + .default(false) + .hideHelp(), + ) + .action(async (options) => { + const { packageManager, codeEditor, includeFunctionalTests } = options; + + const URLSchema = z.string().url(); + const sampleDataApiUrl = parse(options.sampleDataApiUrl, URLSchema); + const bigcommerceApiUrl = parse(`https://api.${options.bigcommerceHostname}`, URLSchema); + const bigcommerceAuthUrl = parse(`https://login.${options.bigcommerceHostname}`, URLSchema); + + let ghRef: string; + + if (options.ghRef instanceof Function) { + ghRef = await options.ghRef(); + } else { + ghRef = options.ghRef; + } - if (!pathExistsSync(options.projectDir)) { - console.error(chalk.red(`Error: --projectDir ${options.projectDir} is not a valid path\n`)); - process.exit(1); - } + let projectName; + let projectDir; + let storeHash = options.storeHash; + let accessToken = options.accessToken; + let channelId; + let customerImpersonationToken = options.customerImpersonationToken; - if (options.projectName) { - projectName = kebabCase(options.projectName); - projectDir = join(options.projectDir, projectName); + if (options.channelId) { + channelId = parseInt(options.channelId, 10); + } - if (pathExistsSync(projectDir)) { - console.error(chalk.red(`Error: ${projectDir} already exists\n`)); + if (!pathExistsSync(options.projectDir)) { + console.error(chalk.red(`Error: --projectDir ${options.projectDir} is not a valid path\n`)); process.exit(1); } - } - - if (!options.projectName) { - const validateProjectName = (i: string) => { - const formatted = kebabCase(i); - - if (!formatted) return 'Project name is required'; - - const targetDir = join(options.projectDir, formatted); - - if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`; - projectName = formatted; - projectDir = targetDir; + if (options.projectName) { + projectName = kebabCase(options.projectName); + projectDir = join(options.projectDir, projectName); - return true; - }; - - await input({ - message: 'What is the name of your project?', - default: 'my-catalyst-app', - validate: validateProjectName, - }); - } - - if (!options.storeHash || !options.accessToken) { - const credentials = await login(bigcommerceAuthUrl); - - storeHash = credentials.storeHash; - accessToken = credentials.accessToken; - } - - if (!projectName) throw new Error('Something went wrong, projectName is not defined'); - if (!projectDir) throw new Error('Something went wrong, projectDir is not defined'); - - if (!storeHash || !accessToken) { - console.log(`\nCreating '${projectName}' at '${projectDir}'\n`); - - await cloneCatalyst({ projectDir, projectName, ghRef, codeEditor, includeFunctionalTests }); - - console.log(`\nUsing ${chalk.bold(packageManager)}\n`); + if (pathExistsSync(projectDir)) { + console.error(chalk.red(`Error: ${projectDir} already exists\n`)); + process.exit(1); + } + } - await installDependencies(projectDir, packageManager); + if (!options.projectName) { + const validateProjectName = (i: string) => { + const formatted = kebabCase(i); - console.log( - [ - `\n${chalk.green('Success!')} Created '${projectName}' at '${projectDir}'\n`, - `Next steps:`, - chalk.yellow(`\n- cd ${projectName} && cp .env.example .env.local`), - chalk.yellow(`\n- Populate .env.local with your BigCommerce API credentials\n`), - ].join('\n'), - ); + if (!formatted) return 'Project name is required'; - process.exit(0); - } + const targetDir = join(options.projectDir, formatted); - if (!channelId || !customerImpersonationToken) { - const bc = new Https({ bigCommerceApiUrl: bigcommerceApiUrl, storeHash, accessToken }); - const availableChannels = await bc.channels('?available=true&type=storefront'); - const storeInfo = await bc.storeInformation(); + if (pathExistsSync(targetDir)) return `Destination '${targetDir}' already exists`; - const canCreateChannel = checkStorefrontLimit(availableChannels, storeInfo); + projectName = formatted; + projectDir = targetDir; - let shouldCreateChannel; + return true; + }; - if (canCreateChannel) { - shouldCreateChannel = await select({ - message: 'Would you like to create a new channel?', - choices: [ - { name: 'Yes', value: true }, - { name: 'No', value: false }, - ], + await input({ + message: 'What is the name of your project?', + default: 'my-catalyst-app', + validate: validateProjectName, }); } - if (shouldCreateChannel) { - const newChannelName = await input({ - message: 'What would you like to name your new channel?', - }); + if (!options.storeHash || !options.accessToken) { + const credentials = await login(bigcommerceAuthUrl); - const sampleDataApi = new Https({ - sampleDataApiUrl, - storeHash, - accessToken, - }); + storeHash = credentials.storeHash; + accessToken = credentials.accessToken; + } - const { - data: { id: createdChannelId, storefront_api_token: storefrontApiToken }, - } = await sampleDataApi.createChannel(newChannelName); + if (!projectName) throw new Error('Something went wrong, projectName is not defined'); + if (!projectDir) throw new Error('Something went wrong, projectDir is not defined'); - await bc.createChannelMenus(createdChannelId); + if (!storeHash || !accessToken) { + console.log(`\nCreating '${projectName}' at '${projectDir}'\n`); - channelId = createdChannelId; - customerImpersonationToken = storefrontApiToken; + await cloneCatalyst({ projectDir, projectName, ghRef, codeEditor, includeFunctionalTests }); - /** - * @todo prompt sample data API - */ - } + console.log(`\nUsing ${chalk.bold(packageManager)}\n`); - if (!shouldCreateChannel) { - const channelSortOrder = ['catalyst', 'next', 'bigcommerce']; - - const existingChannel = await select({ - message: 'Which channel would you like to use?', - choices: availableChannels.data - .sort( - (a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform), - ) - .map((ch) => ({ - name: ch.name, - value: ch, - description: `Channel Platform: ${ - ch.platform === 'bigcommerce' - ? 'Stencil' - : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1) - }`, - })), - }); + await installDependencies(projectDir, packageManager); - channelId = existingChannel.id; + console.log( + [ + `\n${chalk.green('Success!')} Created '${projectName}' at '${projectDir}'\n`, + `Next steps:`, + chalk.yellow(`\n- cd ${projectName} && cp .env.example .env.local`), + chalk.yellow(`\n- Populate .env.local with your BigCommerce API credentials\n`), + ].join('\n'), + ); - const { - data: { token }, - } = await bc.customerImpersonationToken(channelId); + process.exit(0); + } - customerImpersonationToken = token; + if (!channelId || !customerImpersonationToken) { + const bc = new Https({ bigCommerceApiUrl: bigcommerceApiUrl, storeHash, accessToken }); + const availableChannels = await bc.channels('?available=true&type=storefront'); + const storeInfo = await bc.storeInformation(); + + const canCreateChannel = checkStorefrontLimit(availableChannels, storeInfo); + + let shouldCreateChannel; + + if (canCreateChannel) { + shouldCreateChannel = await select({ + message: 'Would you like to create a new channel?', + choices: [ + { name: 'Yes', value: true }, + { name: 'No', value: false }, + ], + }); + } + + if (shouldCreateChannel) { + const newChannelName = await input({ + message: 'What would you like to name your new channel?', + }); + + const sampleDataApi = new Https({ + sampleDataApiUrl, + storeHash, + accessToken, + }); + + const { + data: { id: createdChannelId, storefront_api_token: storefrontApiToken }, + } = await sampleDataApi.createChannel(newChannelName); + + await bc.createChannelMenus(createdChannelId); + + channelId = createdChannelId; + customerImpersonationToken = storefrontApiToken; + + /** + * @todo prompt sample data API + */ + } + + if (!shouldCreateChannel) { + const channelSortOrder = ['catalyst', 'next', 'bigcommerce']; + + const existingChannel = await select({ + message: 'Which channel would you like to use?', + choices: availableChannels.data + .sort( + (a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform), + ) + .map((ch) => ({ + name: ch.name, + value: ch, + description: `Channel Platform: ${ + ch.platform === 'bigcommerce' + ? 'Stencil' + : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1) + }`, + })), + }); + + channelId = existingChannel.id; + + const { + data: { token }, + } = await bc.customerImpersonationToken(channelId); + + customerImpersonationToken = token; + } } - } - if (!channelId) throw new Error('Something went wrong, channelId is not defined'); - if (!customerImpersonationToken) - throw new Error('Something went wrong, customerImpersonationToken is not defined'); + if (!channelId) throw new Error('Something went wrong, channelId is not defined'); + if (!customerImpersonationToken) + throw new Error('Something went wrong, customerImpersonationToken is not defined'); - console.log(`\nCreating '${projectName}' at '${projectDir}'\n`); + console.log(`\nCreating '${projectName}' at '${projectDir}'\n`); - await cloneCatalyst({ projectDir, projectName, ghRef, codeEditor, includeFunctionalTests }); + await cloneCatalyst({ projectDir, projectName, ghRef, codeEditor, includeFunctionalTests }); - writeEnv(projectDir, { - channelId: channelId.toString(), - storeHash, - accessToken, - customerImpersonationToken, - }); + writeEnv(projectDir, { + channelId: channelId.toString(), + storeHash, + accessToken, + customerImpersonationToken, + }); + + console.log(`\nUsing ${chalk.bold(packageManager)}\n`); - console.log(`\nUsing ${chalk.bold(packageManager)}\n`); + await installDependencies(projectDir, packageManager); - await installDependencies(projectDir, packageManager); + await spinner(exec(`${packageManager} run --prefix ${projectDir} generate`), { + text: 'Creating GraphQL schema...', + successText: 'Created GraphQL schema', + failText: (err) => chalk.red(`Failed to create GraphQL schema: ${err.message}`), + }); - await spinner(exec(`${packageManager} run --prefix ${projectDir} generate`), { - text: 'Creating GraphQL schema...', - successText: 'Created GraphQL schema', - failText: (err) => chalk.red(`Failed to create GraphQL schema: ${err.message}`), + console.log( + `\n${chalk.green('Success!')} Created '${projectName}' at '${projectDir}'\n`, + '\nNext steps:\n', + chalk.yellow(`\ncd ${projectName} && ${packageManager} run dev\n`), + ); }); - - console.log( - `\n${chalk.green('Success!')} Created '${projectName}' at '${projectDir}'\n`, - '\nNext steps:\n', - chalk.yellow(`\ncd ${projectName} && ${packageManager} run dev\n`), - ); -}; diff --git a/packages/create-catalyst/src/commands/init.ts b/packages/create-catalyst/src/commands/init.ts index 87508fe74..bd329756c 100644 --- a/packages/create-catalyst/src/commands/init.ts +++ b/packages/create-catalyst/src/commands/init.ts @@ -1,119 +1,135 @@ +import { Command, Option } from '@commander-js/extra-typings'; import { input, select } from '@inquirer/prompts'; import chalk from 'chalk'; import * as z from 'zod'; -import { type InitCommandOptions } from '../index'; import { checkStorefrontLimit } from '../utils/check-storefront-limit'; import { Https } from '../utils/https'; import { login } from '../utils/login'; import { parse } from '../utils/parse'; import { writeEnv } from '../utils/write-env'; -export const init = async (options: InitCommandOptions) => { - const projectDir = process.cwd(); - - const URLSchema = z.string().url(); - const sampleDataApiUrl = parse(options.sampleDataApiUrl, URLSchema); - const bigCommerceApiUrl = `https://api.${options.bigcommerceHostname}`; - const bigCommerceAuthUrl = `https://login.${options.bigcommerceHostname}`; - - let storeHash = options.storeHash; - let accessToken = options.accessToken; - let channelId; - let customerImpersonationToken; - - if (!options.storeHash || !options.accessToken) { - const credentials = await login(bigCommerceAuthUrl); - - storeHash = credentials.storeHash; - accessToken = credentials.accessToken; - } - - if (!storeHash || !accessToken) { - console.log( - chalk.yellow('\nYou must authenticate with a store to overwrite your local environment.\n'), - ); - - process.exit(1); - } - - const bc = new Https({ bigCommerceApiUrl, storeHash, accessToken }); - - const availableChannels = await bc.channels('?available=true&type=storefront'); - const storeInfo = await bc.storeInformation(); - - const canCreateChannel = checkStorefrontLimit(availableChannels, storeInfo); - - let shouldCreateChannel; - - if (canCreateChannel) { - shouldCreateChannel = await select({ - message: 'Would you like to create a new channel?', - choices: [ - { name: 'Yes', value: true }, - { name: 'No', value: false }, - ], - }); - } - - if (shouldCreateChannel) { - const newChannelName = await input({ - message: 'What would you like to name your new channel?', - }); - - const sampleDataApi = new Https({ - sampleDataApiUrl, +export const init = new Command('init') + .description('Connect a BigCommerce store with an existing Catalyst project') + .option('--store-hash ', 'BigCommerce store hash') + .option('--access-token ', 'BigCommerce access token') + .addOption( + new Option('--bigcommerce-hostname ', 'BigCommerce hostname') + .default('bigcommerce.com') + .hideHelp(), + ) + .addOption( + new Option('--sample-data-api-url ', 'BigCommerce sample data API URL') + .default('https://api.bc-sample.store') + .hideHelp(), + ) + .action(async (options) => { + const projectDir = process.cwd(); + + const URLSchema = z.string().url(); + const sampleDataApiUrl = parse(options.sampleDataApiUrl, URLSchema); + const bigCommerceApiUrl = `https://api.${options.bigcommerceHostname}`; + const bigCommerceAuthUrl = `https://login.${options.bigcommerceHostname}`; + + let storeHash = options.storeHash; + let accessToken = options.accessToken; + let channelId; + let customerImpersonationToken; + + if (!options.storeHash || !options.accessToken) { + const credentials = await login(bigCommerceAuthUrl); + + storeHash = credentials.storeHash; + accessToken = credentials.accessToken; + } + + if (!storeHash || !accessToken) { + console.log( + chalk.yellow('\nYou must authenticate with a store to overwrite your local environment.\n'), + ); + + process.exit(1); + } + + const bc = new Https({ bigCommerceApiUrl, storeHash, accessToken }); + + const availableChannels = await bc.channels('?available=true&type=storefront'); + const storeInfo = await bc.storeInformation(); + + const canCreateChannel = checkStorefrontLimit(availableChannels, storeInfo); + + let shouldCreateChannel; + + if (canCreateChannel) { + shouldCreateChannel = await select({ + message: 'Would you like to create a new channel?', + choices: [ + { name: 'Yes', value: true }, + { name: 'No', value: false }, + ], + }); + } + + if (shouldCreateChannel) { + const newChannelName = await input({ + message: 'What would you like to name your new channel?', + }); + + const sampleDataApi = new Https({ + sampleDataApiUrl, + storeHash, + accessToken, + }); + + const { + data: { id: createdChannelId, storefront_api_token: storefrontApiToken }, + } = await sampleDataApi.createChannel(newChannelName); + + channelId = createdChannelId; + customerImpersonationToken = storefrontApiToken; + + /** + * @todo prompt sample data API + */ + } + + if (!shouldCreateChannel) { + const channelSortOrder = ['catalyst', 'next', 'bigcommerce']; + + const existingChannel = await select({ + message: 'Which channel would you like to use?', + choices: availableChannels.data + .sort( + (a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform), + ) + .map((ch) => ({ + name: ch.name, + value: ch, + description: `Channel Platform: ${ + ch.platform === 'bigcommerce' + ? 'Stencil' + : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1) + }`, + })), + }); + + channelId = existingChannel.id; + + const { + data: { token }, + } = await bc.customerImpersonationToken(channelId); + + customerImpersonationToken = token; + } + + if (!channelId) throw new Error('Something went wrong, channelId is not defined'); + if (!customerImpersonationToken) + throw new Error('Something went wrong, customerImpersonationToken is not defined'); + + writeEnv(projectDir, { + channelId: channelId.toString(), storeHash, accessToken, + customerImpersonationToken, }); - - const { - data: { id: createdChannelId, storefront_api_token: storefrontApiToken }, - } = await sampleDataApi.createChannel(newChannelName); - - channelId = createdChannelId; - customerImpersonationToken = storefrontApiToken; - - /** - * @todo prompt sample data API - */ - } - - if (!shouldCreateChannel) { - const channelSortOrder = ['catalyst', 'next', 'bigcommerce']; - - const existingChannel = await select({ - message: 'Which channel would you like to use?', - choices: availableChannels.data - .sort((a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform)) - .map((ch) => ({ - name: ch.name, - value: ch, - description: `Channel Platform: ${ - ch.platform === 'bigcommerce' - ? 'Stencil' - : ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1) - }`, - })), - }); - - channelId = existingChannel.id; - - const { - data: { token }, - } = await bc.customerImpersonationToken(channelId); - - customerImpersonationToken = token; - } - - if (!channelId) throw new Error('Something went wrong, channelId is not defined'); - if (!customerImpersonationToken) - throw new Error('Something went wrong, customerImpersonationToken is not defined'); - - writeEnv(projectDir, { - channelId: channelId.toString(), - storeHash, - accessToken, - customerImpersonationToken, }); -}; diff --git a/packages/create-catalyst/src/index.ts b/packages/create-catalyst/src/index.ts index 885d89197..886c30446 100644 --- a/packages/create-catalyst/src/index.ts +++ b/packages/create-catalyst/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { Command, Option } from '@commander-js/extra-typings'; +import { program } from '@commander-js/extra-typings'; import chalk from 'chalk'; import { satisfies } from 'semver'; @@ -8,8 +8,6 @@ import PACKAGE_INFO from '../package.json'; import { create } from './commands/create'; import { init } from './commands/init'; -import { getLatestCoreTag } from './utils/get-latest-core-tag'; -import { getPackageManager, packageManagerChoices } from './utils/pm'; if (!satisfies(process.version, PACKAGE_INFO.engines.node)) { console.error( @@ -27,76 +25,11 @@ if (!satisfies(process.version, PACKAGE_INFO.engines.node)) { console.log(chalk.cyanBright(`\n◢ ${PACKAGE_INFO.name} v${PACKAGE_INFO.version}\n`)); -export type Options = T extends Command ? [...Args, Opts, T] : never; - -const program = new Command() +program .name(PACKAGE_INFO.name) .version(PACKAGE_INFO.version) - .description('A command line tool to create a new Catalyst project.'); - -const createCommand = program - .command('create', { isDefault: true }) - .description('Command to scaffold and connect a Catalyst storefront to your BigCommerce store') - .option('--project-name ', 'Name of your Catalyst project') - .option('--project-dir ', 'Directory in which to create your project', process.cwd()) - .option('--store-hash ', 'BigCommerce store hash') - .option('--access-token ', 'BigCommerce access token') - .option('--channel-id ', 'BigCommerce channel ID') - .option('--customer-impersonation-token ', 'BigCommerce customer impersonation token') - .addOption( - new Option( - '--gh-ref ', - 'Clone a specific ref from the bigcommerce/catalyst repository', - ).default(getLatestCoreTag), - ) - .addOption( - new Option('--bigcommerce-hostname ', 'BigCommerce hostname') - .default('bigcommerce.com') - .hideHelp(), - ) - .addOption( - new Option('--sample-data-api-url ', 'BigCommerce sample data API URL') - .default('https://api.bc-sample.store') - .hideHelp(), - ) - .addOption( - new Option('--package-manager ', 'Override detected package manager') - .choices(packageManagerChoices) - .default(getPackageManager()) - .hideHelp(), - ) - .addOption( - new Option('--code-editor ', 'Your preferred code editor') - .choices(['vscode']) - .default('vscode') - .hideHelp(), - ) - .addOption( - new Option('--include-functional-tests', 'Include the functional test suite') - .default(false) - .hideHelp(), - ) - .action((opts) => create(opts)); - -export type CreateCommandOptions = Options[0]; - -const initCommand = program - .command('init') - .description('Connect a BigCommerce store with an existing Catalyst project') - .option('--store-hash ', 'BigCommerce store hash') - .option('--access-token ', 'BigCommerce access token') - .addOption( - new Option('--bigcommerce-hostname ', 'BigCommerce hostname') - .default('bigcommerce.com') - .hideHelp(), - ) - .addOption( - new Option('--sample-data-api-url ', 'BigCommerce sample data API URL') - .default('https://api.bc-sample.store') - .hideHelp(), - ) - .action((opts) => init(opts)); - -export type InitCommandOptions = Options[0]; + .description('A command line tool to create a new Catalyst project.') + .addCommand(create, { isDefault: true }) + .addCommand(init); program.parse(process.argv);