UNPKG

@swell/cli

Version:

Swell's command line interface/utility

254 lines (253 loc) 9.87 kB
import { confirm, select } from '@inquirer/prompts'; import { Flags } from '@oclif/core'; import { $ } from 'execa'; import { exec } from 'node:child_process'; import path from 'node:path'; import Stream from 'node:stream'; import { promisify } from 'node:util'; import ora from 'ora'; import Api from './lib/api.js'; import { FrontendProjectTypes, getAllConfigPaths, writeFile, writeJsonFile, } from './lib/apps/index.js'; import style from './lib/style.js'; import { SwellCommand } from './swell-command.js'; const execAsync = promisify(exec); export class CreateAppCommand extends SwellCommand { static baseFlags = { frontend: Flags.string({ description: `create a starter framework for a hosted frontend`, options: FrontendProjectTypes.map(({ slug }) => slug), }), 'storefront-app': Flags.string({ description: `id of an installed storefront app to create a theme for, if applicable`, }), yes: Flags.boolean({ char: 'y', description: `accept all default values`, }), }; devApi; constructor(argv, config) { super(argv, config); this.devApi = new Api(undefined, 'test'); } async createAppConfigFolders(swellConfig) { for (const type of getAllConfigPaths(swellConfig.get('type'))) { // Create a config folder // eslint-disable-next-line no-await-in-loop await execAsync(`mkdir -p ${type}`, { cwd: path.dirname(swellConfig.path), }); } } async createFrontendApp(swellConfig, flags, directCreate, kind) { const spinner = ora(); const configPath = path.dirname(swellConfig.path); const frameworkType = flags.frontend || (await select({ choices: [ ...FrontendProjectTypes.map(({ name, slug }) => ({ name, value: slug, })), ...(directCreate ? [] : [ { name: 'Do not install a frontend framework', value: null, }, ]), ], message: directCreate ? `Choose framework for your hosted${kind ? ` ${kind} ` : ' '}app frontend` : `Install framework for a hosted${kind ? ` ${kind} ` : ' '}app frontend?`, })); if (frameworkType) { const projectType = FrontendProjectTypes.find((type) => type.slug === frameworkType); if (!projectType) { this.error(`Could not find project type: ${frameworkType}`); } this.log(); spinner.start(`Creating ${projectType?.name} frontend app (this may take a while)...`); try { await execAsync(`mkdir -p frontend`, { cwd: configPath, }); await execAsync(projectType.installCommand, { cwd: configPath, }); // Use this command to debug output, i.e.e when command becomes non-responsive /* await this.execWithStdio( configPath, projectType.installCommand, ); */ } catch (error) { spinner.fail(`Error creating ${projectType?.name} app:\n`); this.log(error.stdout); this.log(); return false; } spinner.succeed(`${projectType?.name} initialized app in ${configPath}/frontend/`); if (directCreate) { this.log(); } return true; } return false; } async createStorefrontApp(swellConfig, flags, directCreate) { return this.createFrontendApp(swellConfig, flags, directCreate, 'storefront'); } async createThemeApp(config, flags, installedStorefrontApp) { const spinner = ora(); const configPath = path.dirname(config.path); const defaultThemeConfigs = await this.devApi.get({ adminPath: `/client/apps/${installedStorefrontApp.app_id}/theme-template-configs`, }); if (defaultThemeConfigs.results?.length > 0) { const { name: appName, version: appVersion } = installedStorefrontApp.app; const shouldCreate = flags.yes || (await confirm({ message: `Create a theme template for ${style.appConfigValue(appName)} ${appVersion ? `v${appVersion}` : ''}?`, })); if (shouldCreate) { this.log(); spinner.start(`Creating theme template...`); try { await execAsync(`mkdir -p theme`, { cwd: configPath, }); for (const themeConfig of defaultThemeConfigs.results) { const filePath = themeConfig.file_path.replace(/^frontend\/theme-template\//, ''); const fileContent = themeConfig.file_data; const isJson = filePath.endsWith('.json'); // eslint-disable-next-line no-await-in-loop await execAsync(`mkdir -p ${path.dirname(filePath)}`, { cwd: configPath, }); // eslint-disable-next-line no-await-in-loop await (isJson ? writeJsonFile(path.join(configPath, filePath), fileContent) : writeFile(path.join(configPath, filePath), fileContent)); } } catch (error) { spinner.fail(`Error creating theme template:\n`); this.log(error.stdout); this.log(); return false; } spinner.succeed(`Theme template initialized in ${configPath}/theme/`); } } return true; } async doesPackageManagerExist(packageManager) { try { await execAsync(`${packageManager} --version`); return true; } catch { return false; } } async execWithStdio(cwd, command, onOutput) { const $$ = $({ cwd, shell: true, stderr: 'inherit', stdin: 'inherit', stdout: 'pipe', }); const outStream = new Stream.Writable(); outStream._write = (chunk, _encoding, next) => { const string = chunk.toString(); const out = onOutput?.(string); if (out !== false) { console.log(string); } next(); }; try { await $$ `${command}`.pipeStdout?.(outStream); } catch (error) { console.log(error); } } async findPackageManager(pkg = '') { const packageManager = pkg; // Determine if system has yarn or npm install if (packageManager) { if (await this.doesPackageManagerExist(packageManager)) { return packageManager; } } else { if (await this.doesPackageManagerExist('npm')) { return 'npm'; } if (await this.doesPackageManagerExist('yarn')) { return 'yarn'; } } } async getInstalledStorefrontApps() { const spinner = ora(); spinner.start('Retrieving installed storefront apps...'); const installedApps = await this.devApi.get({ adminPath: `/client/apps`, }); const storefrontInstalledApps = installedApps.results.filter((installedApp) => installedApp.app?.type === 'storefront'); if (storefrontInstalledApps.length === 0) { spinner.fail(`You must have at least one storefront app installed in your ${style.appEnv('test')} environment before creating a theme.`); return false; } spinner.stop(); return storefrontInstalledApps; } async setupPackage(name, config, pkg) { const execAsync = promisify(exec); const configPath = path.dirname(config.path); const packageJson = { description: config.get('description'), devDependencies: { '@swell/app-types': '^1.0.5', }, name, scripts: {}, version: config.get('version'), }; const tsConfig = { compilerOptions: { lib: ['esnext', 'webworker'], module: 'esnext', target: 'esnext', types: ['@swell/app-types'], }, exclude: ['node_modules'], include: ['**/*.ts'], }; await writeJsonFile(path.join(configPath, 'package.json'), packageJson); await writeJsonFile(path.join(configPath, 'tsconfig.json'), tsConfig); await writeFile(path.join(configPath, '.gitignore'), `node_modules`); try { const packageManager = await this.findPackageManager(pkg); await execAsync(`${packageManager} install`, { cwd: configPath, }); } catch (error) { this.error(error.message); } } async tryPackageSetup(name, config, pkg) { try { await this.setupPackage(name, config, pkg); } catch (error) { this.log(`There was an error setting up the package manager: ${error.message}`); } } }