@swell/cli
Version:
Swell's command line interface/utility
254 lines (253 loc) • 9.87 kB
JavaScript
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}`);
}
}
}