UNPKG

@swell/cli

Version:

Swell's command line interface/utility

403 lines (402 loc) 17.6 kB
import { confirm, input, select } from '@inquirer/prompts'; import { Args, Flags } from '@oclif/core'; import path from 'node:path'; import ora from 'ora'; import { CreateAppCommand } from '../../create-app-command.js'; import { newConfig, swellConfigFileExists } from '../../lib/app-config.js'; import { toAppId, toAppName } from '../../lib/create/index.js'; import style from '../../lib/style.js'; /** * Shared helpMeta for app creation commands (create app, app init). * Defines the APP TYPES section and type-specific flags. */ export const appHelpMetaBase = { typeSection: { title: 'APP TYPES', types: [ { name: 'admin', description: 'Dashboard extension', optional: ['--frontend'], }, { name: 'storefront', description: 'Hosted storefront', optional: ['--frontend'], }, { name: 'theme', description: 'Theme for existing storefront', requires: ['--storefront-app'], }, { name: 'integration', description: 'Third-party service', requires: [ '--integration-type', '--integration-id (for payment | shipping | tax)', ], }, ], }, typeFlags: [ 'frontend', 'storefront-app', 'integration-type', 'integration-id', ], }; export default class CreateApp extends CreateAppCommand { static args = { id: Args.string({ default: '', description: 'App identifier (e.g., my-app)', }), }; static description = 'Create an app.'; static examples = [ '$ swell create app', '$ swell create app my_app -t admin -y', '$ swell create app my_app -t admin --frontend astro -y', '$ swell create app my_theme -t theme --storefront-app proxima -y', '$ swell create app my_payment -t integration --integration-type payment --integration-id card -y', '$ swell create app my_shipping -t integration --integration-type shipping --integration-id fedex -y', ]; static helpMeta = { ...appHelpMetaBase, usageDirect: '<id> -t <type> [...] -y', }; static flags = { pkg: Flags.string({ char: 'p', default: 'npm', description: 'Package manager: npm | yarn | none', options: ['npm', 'yarn', 'none'], }), type: Flags.string({ char: 't', description: 'App type: admin | storefront | theme | integration', options: ['admin', 'integration', 'storefront', 'theme'], }), name: Flags.string({ char: 'n', description: 'Display name (default: capitalized ID)', }), version: Flags.string({ char: 'v', description: 'Version (default: 1.0.0)', }), description: Flags.string({ char: 'd', description: 'Description', }), }; appType = ''; async createSwellConfig({ allowOverwrite = true, inputId = '', inputStorefrontApp, inputType, inputName, inputVersion, inputDescription, inputFrontend, inputIntegrationType, inputIntegrationId, inputYes, nestedPath = true, }) { const confirmYes = inputYes; let appId = toAppId(inputId || ''); let storefrontInstalledApps; let storefrontAppId; let installedStorefrontApp; // check missed parameters if (confirmYes) { if (!appId) { this.error(`Missing application id for non-interactive mode\n\nYou need to provide an app id value for non-interactive mode\n\nExample: ${this.commandExample} reviews --type admin -y`, { exit: 1, }); } if (!inputType) { this.error(`Missing required flag for non-interactive mode: --type\n\nValid types: admin, integration, storefront, theme\n\nExample: ${this.commandExample} reviews --type admin -y`, { exit: 1, }); } if (!inputName) { inputName = toAppName(inputId); } if (!inputVersion) { inputVersion = '1.0.0'; } } const type = inputType || (await select({ choices: [ { name: 'Admin', value: 'admin' }, { name: 'Integration', value: 'integration' }, { name: 'Storefront', value: 'storefront' }, { name: 'Theme', value: 'theme' }, ], message: `What is the primary purpose of the app?`, })); const typeLabel = type === 'theme' ? 'theme' : 'app'; const typeLabelTitle = type === 'theme' ? 'Theme' : 'App'; const name = inputName || (await input({ default: inputId ? toAppName(inputId) : '', message: `${typeLabelTitle} name`, validate: (answer) => answer.length > 2, })); if (!appId) { appId = await input({ default: toAppId(name), message: `${typeLabelTitle} ID`, validate: (answer) => answer.length > 2, }); appId = toAppId(appId); } const version = inputVersion || (await input({ default: '1.0.0', message: `${typeLabelTitle} version`, })); const description = inputDescription || (confirmYes ? '' : await input({ default: 'Something special', message: `Describe your ${typeLabel}`, })); let integrationType; let extensions; if (type === 'integration') { if (confirmYes && !inputIntegrationType) { this.error(`Missing required flag for non-interactive mode for integration app: --integration-type\n\nValid types: generic, payment, shipping, tax\n\nExample: ${this.commandExample} reviews --type integration --integration-type generic -y`, { exit: 1, }); } integrationType = inputIntegrationType || (await select({ choices: [ { name: 'Generic', value: 'generic' }, { name: 'Payment gateway', value: 'payment' }, { name: 'Shipping service', value: 'shipping' }, { name: 'Tax service', value: 'tax' }, ], message: `What type of integration?`, })); // Create extensions array based on integration type switch (integrationType) { case 'generic': { // No extension needed for generic integrations break; } case 'payment': case 'shipping': case 'tax': { // Unified validation for all extension-based integrations if (confirmYes && !inputIntegrationId) { const validValues = integrationType === 'payment' ? "use 'card' for credit card integration or unique custom ID for other payment integrations" : 'unique custom ID'; this.error(`Missing required flag for non-interactive mode for ${integrationType} integration: --integration-id\n\nValid values: ${validValues}\n\nExample: ${this.commandExample} my_app --type integration --integration-type ${integrationType} --integration-id ${integrationType === 'payment' ? 'card' : 'myid'} -y`, { exit: 1, }); } // Interactive prompting let integrationId = inputIntegrationId; if (!integrationId) { if (integrationType === 'payment') { const methodChoice = await select({ choices: [ { name: 'Credit card', value: 'card' }, { name: 'Custom method', value: 'custom' }, ], message: `What type of payment method?`, }); integrationId = methodChoice === 'custom' ? await input({ message: `Enter a unique ID for your payment method`, validate: (answer) => answer.length > 2, }) : methodChoice; // 'card' } else { // Shipping or tax const serviceLabel = integrationType === 'shipping' ? 'shipping service' : 'tax service'; integrationId = await input({ message: `Enter a unique ID for your ${serviceLabel}`, validate: (answer) => answer.length > 2, }); } } // Unified validation if (integrationId.length < 3) { this.error(`--integration-id must contain at least 3 characters\n\nValid value is a custom ID containing at least 3 characters\n\nExample: ${this.commandExample} my_app --type integration --integration-type ${integrationType} --integration-id ${integrationType === 'payment' ? 'card' : 'myid'} -y`, { exit: 1, }); } // Create extension extensions = [ { id: integrationId, type: integrationType, }, ]; break; } default: { this.error(`Incorrect value for --integration-type\n\nValid types: generic, payment, shipping, tax\n\nExample: ${this.commandExample} reviews --type integration --integration-type generic -y`, { exit: 1, }); } } } if (type === 'theme') { storefrontInstalledApps = await this.getInstalledStorefrontApps(); if (storefrontInstalledApps === false) { this.error('There are no installed storefront apps\n\nTo use the theme type, you first need to install the storefront app', { exit: 1, }); } storefrontAppId = inputStorefrontApp || ''; if (!storefrontAppId) { if (storefrontInstalledApps.length === 1) { storefrontAppId = storefrontInstalledApps[0].id; } else { if (confirmYes) { this.error(`There are several installed storefront apps to choose from\n\nYou should select a specific storefront app in the non-interactive mode using --storefront-app appid\n\nExample: ${this.commandExample} reviews --type theme --storefront-app proxima -y`, { exit: 1, }); } storefrontAppId = await select({ choices: storefrontInstalledApps.map(({ app, id }) => ({ name: app.name, value: id, })), message: `Which storefront app is your theme for?`, }); } } installedStorefrontApp = storefrontInstalledApps.find((installedApp) => inputStorefrontApp ? inputStorefrontApp === installedApp.app_private_id || inputStorefrontApp === installedApp.app_public_id || inputStorefrontApp === installedApp.app_id || inputStorefrontApp === toAppId(installedApp.app_private_id) : installedApp.id === storefrontAppId); if (!installedStorefrontApp) { this.error(`Could not find an installed storefront app by id: ${storefrontAppId}\n\nThe selected storefront app is not among the installed storefront apps`, { exit: 1, }); } } // check inputFrontend value if (!installedStorefrontApp && confirmYes && inputFrontend && inputFrontend !== 'none') { const projectType = this.getProjectType(inputFrontend); if (!projectType) { // error should be thrown from getProjectType return; } } const swellConfigJson = { description, id: appId, name, type, version, ...(type === 'theme' ? { theme: { storefront: { app: toAppId(installedStorefrontApp.app_private_id), }, }, } : { permissions: [], }), ...(extensions && { extensions }), }; const appPath = nestedPath ? path.join(process.cwd(), inputId || appId) : process.cwd(); const swellConfigPath = path.join(appPath, 'swell.json'); if (swellConfigFileExists(swellConfigPath)) { if (allowOverwrite) { if (!confirmYes) { const continueCreateApp = await confirm({ message: `An app already exists in the target path ${appPath}. Overwrite?`, }); if (!continueCreateApp) { return; } } } else { this.error(`An app already exists in the target path ${appPath}.\n\nCannot overwrite existing app`, { exit: 1, }); } } this.log(`\nCreating ${this.appType === 'theme' ? 'theme' : 'app'} ${style.appConfigValue(name)} in ${appPath}/swell.json`); this.log(`\n${JSON.stringify(swellConfigJson, null, 2)}\n`); const confirmCreateApp = confirmYes ? true : await confirm({ message: 'Ok to continue?', }); if (!confirmCreateApp) { return; } const config = newConfig(nestedPath ? inputId || appId : ''); config.clear(); config.set(swellConfigJson); return { appId, config, installedStorefrontApp, swellConfigJson }; } async run() { const { args, flags } = await this.parse(CreateApp); const { id } = args; const { pkg, yes, name, type, version, description, frontend } = flags; const confirmYes = Boolean(yes); const createParams = await this.createSwellConfig({ inputId: id, inputStorefrontApp: flags['storefront-app'], inputType: type, inputYes: confirmYes, inputName: name, inputVersion: version, inputDescription: description, inputFrontend: frontend, inputIntegrationType: flags['integration-type'], inputIntegrationId: flags['integration-id'], }); if (!createParams) { return; } const { appId, config, installedStorefrontApp } = createParams; !confirmYes && this.log(); const spinner = ora(); spinner.start('Creating app...'); if (pkg !== 'none') { await this.tryPackageSetup(appId, config, pkg); } await this.createAppConfigFolders(config); spinner.succeed(`${style.appConfigValue(config.get('name'))} app created.\n`); let createdFrontend; let createdTheme; if (installedStorefrontApp) { createdTheme = await this.createThemeApp(config, flags, installedStorefrontApp); } else if (config.get('type') === 'storefront') { createdFrontend = await this.createStorefrontApp(config, flags); } else { createdFrontend = await this.createFrontendApp(config, flags); } this.log('\nNext steps:'); this.log(`Run ${style.command('swell app push')} to push configurations to your test store.`); this.log(`Run ${style.command('swell app install')} to install the app in another store or environment.`); if (createdFrontend) { this.log(`Run ${style.command('swell app frontend dev')} to start a local dev server for your frontend app.`); } else if (createdTheme) { this.log(`Run ${style.command('swell app theme dev')} to start a local dev server for your theme.`); } } }