From 0f56a1089526869ad80e20df5dbec10288ac16eb Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 4 Aug 2022 13:16:16 +0200 Subject: [PATCH 1/3] improve sandbox command error handling and debugging --- scripts/sandbox.ts | 24 +++++++++++++++--- scripts/utils/cli-step.ts | 2 ++ scripts/utils/exec.ts | 53 +++++++++++++++++---------------------- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/scripts/sandbox.ts b/scripts/sandbox.ts index 85517821a594..d882d969adc1 100644 --- a/scripts/sandbox.ts +++ b/scripts/sandbox.ts @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-syntax, no-await-in-loop */ import path from 'path'; -import { remove, pathExists, readJSON, writeJSON, ensureSymlink } from 'fs-extra'; +import { remove, pathExists, readJSON, writeJSON, ensureSymlink, ensureDir } from 'fs-extra'; import prompts from 'prompts'; import { getOptionsOrPrompt } from './utils/options'; @@ -75,6 +75,10 @@ async function getOptions() { dryRun: { description: "Don't execute commands, just list them (dry run)?", }, + debug: { + description: 'Print all the logs to the console', + promptType: false, + }, }); } @@ -147,6 +151,12 @@ async function addPackageScripts({ async function readMainConfig({ cwd }: { cwd: string }) { const configDir = path.join(cwd, '.storybook'); + if (!pathExists(configDir)) { + throw new Error( + `Unable to find the Storybook folder in "${configDir}". Are you sure it exists? Or maybe this folder uses a custom Storybook config directory?` + ); + } + const mainConfigPath = getInterpretedFile(path.resolve(configDir, 'main')); return readConfig(mainConfigPath); } @@ -197,7 +207,10 @@ async function addStories(paths: string[], { mainConfig }: { mainConfig: ConfigF async function main() { const optionValues = await getOptions(); - const { template, forceDelete, forceReuse, link, dryRun } = optionValues; + const { template, forceDelete, forceReuse, link, dryRun, debug } = optionValues; + + await ensureDir(sandboxDir); + const cwd = path.join(sandboxDir, template.replace('/', '-')); const exists = await pathExists(cwd); @@ -222,6 +235,7 @@ async function main() { optionValues: { output: cwd, branch: 'next' }, cwd: sandboxDir, dryRun, + debug, }); const mainConfig = await readMainConfig({ cwd }); @@ -251,7 +265,7 @@ async function main() { for (const addon of optionValues.addon) { const addonName = `@storybook/addon-${addon}`; - await executeCLIStep(steps.add, { argument: addonName, cwd, dryRun }); + await executeCLIStep(steps.add, { argument: addonName, cwd, dryRun, debug }); } for (const addon of [...defaultAddons, ...optionValues.addon]) { @@ -268,6 +282,7 @@ async function main() { cwd: codeDir, dryRun, optionValues: { local: true, start: false }, + debug, }); } else { await exec('yarn local-registry --publish', { cwd: codeDir }, { dryRun }); @@ -313,10 +328,11 @@ async function main() { dryRun, startMessage: `⬆️ Starting Storybook`, errorMessage: `🚨 Starting Storybook failed`, + debug: true, } ); } else { - await executeCLIStep(steps.build, { cwd, dryRun }); + await executeCLIStep(steps.build, { cwd, dryRun, debug }); // TODO serve } diff --git a/scripts/utils/cli-step.ts b/scripts/utils/cli-step.ts index 6d85f0990317..1ac95d379df0 100644 --- a/scripts/utils/cli-step.ts +++ b/scripts/utils/cli-step.ts @@ -19,6 +19,7 @@ export async function executeCLIStep( optionValues?: Partial>; cwd: string; dryRun?: boolean; + debug: boolean; } ) { if (cliStep.hasArgument && !options.argument) @@ -38,6 +39,7 @@ export async function executeCLIStep( startMessage: `${cliStep.icon} ${cliStep.description}`, errorMessage: `🚨 ${cliStep.description} failed`, dryRun: options.dryRun, + debug: options.debug, } ); } diff --git a/scripts/utils/exec.ts b/scripts/utils/exec.ts index 91211783d79a..ed482fb0e06d 100644 --- a/scripts/utils/exec.ts +++ b/scripts/utils/exec.ts @@ -1,17 +1,21 @@ -import shell, { ExecOptions } from 'shelljs'; +import execa, { Options } from 'execa'; import chalk from 'chalk'; const logger = console; +type StepOptions = { + startMessage?: string; + errorMessage?: string; + dryRun?: boolean; + debug?: boolean; +}; + export const exec = async ( command: string, - options: ExecOptions = {}, - { - startMessage, - errorMessage, - dryRun, - }: { startMessage?: string; errorMessage?: string; dryRun?: boolean } = {} -) => { + options: Options = {}, + { startMessage, errorMessage, dryRun, debug }: StepOptions = {} +): Promise => { + logger.info(); if (startMessage) logger.info(startMessage); if (dryRun) { @@ -20,27 +24,16 @@ export const exec = async ( } logger.debug(command); - return new Promise((resolve, reject) => { - const defaultOptions: ExecOptions = { - silent: false, - }; - const child = shell.exec(command, { - ...defaultOptions, - ...options, - async: true, - silent: false, - }); - - child.stderr.pipe(process.stderr); + const defaultOptions: Options = { + stdout: debug ? 'inherit' : 'ignore', + }; + try { + await execa.command(command, { ...defaultOptions, ...options }); + } catch (err) { + logger.error(chalk.red(`An error occurred while executing: \`${command}\``)); + logger.log(errorMessage); + logger.log(err.message); + } - child.on('exit', (code) => { - if (code === 0) { - resolve(undefined); - } else { - logger.error(chalk.red(`An error occurred while executing: \`${command}\``)); - logger.log(errorMessage); - reject(new Error(`command exited with code: ${code}: `)); - } - }); - }); + return undefined; }; From 71fc31706f9c284daefe673513cfd1788d063719 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 5 Aug 2022 20:48:52 +0200 Subject: [PATCH 2/3] utils: throw error rather than just logging it --- scripts/utils/exec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/utils/exec.ts b/scripts/utils/exec.ts index ed482fb0e06d..9680732c27f6 100644 --- a/scripts/utils/exec.ts +++ b/scripts/utils/exec.ts @@ -32,7 +32,7 @@ export const exec = async ( } catch (err) { logger.error(chalk.red(`An error occurred while executing: \`${command}\``)); logger.log(errorMessage); - logger.log(err.message); + throw err; } return undefined; From 4d41f44febbd9ef10e3da16c2df8f408758128e1 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Mon, 8 Aug 2022 18:29:08 +0200 Subject: [PATCH 3/3] sandbox: improve error handling --- code/lib/cli/src/repro-next.ts | 26 ++++++++++++++++---------- scripts/sandbox.ts | 12 ++++++++++-- scripts/utils/options.ts | 12 ++++++++++-- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/code/lib/cli/src/repro-next.ts b/code/lib/cli/src/repro-next.ts index e3fffdf84457..9e1e64ffcf0f 100644 --- a/code/lib/cli/src/repro-next.ts +++ b/code/lib/cli/src/repro-next.ts @@ -1,11 +1,11 @@ import prompts from 'prompts'; -import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; import boxen from 'boxen'; import { dedent } from 'ts-dedent'; import degit from 'degit'; +import { existsSync } from 'fs-extra'; import TEMPLATES from './repro-templates'; const logger = console; @@ -56,9 +56,10 @@ export const reproNext = async ({ boxen( dedent` 🔎 You filtered out all templates. 🔍 + After filtering all the templates with "${chalk.yellow( filterValue - )}", we found no templates. + )}", we found no results. Please try again with a different filter. Available templates: ${keys.map((key) => chalk.blue`- ${key}`).join('\n')} @@ -66,7 +67,7 @@ export const reproNext = async ({ { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any ) ); - return; + process.exit(1); } let selectedTemplate: Choice | null = null; @@ -106,14 +107,19 @@ export const reproNext = async ({ } let selectedDirectory = outputDirectory; + const outputDirectoryName = outputDirectory || selectedTemplate; + if (selectedDirectory && existsSync(`${selectedDirectory}`)) { + logger.info(`⚠️ ${selectedDirectory} already exists! Overwriting...`); + } + if (!selectedDirectory) { const { directory } = await prompts({ type: 'text', message: 'Enter the output directory', name: 'directory', - initial: selectedTemplate, - validate: (directoryName) => - fs.existsSync(directoryName) + initial: outputDirectoryName, + validate: async (directoryName) => + existsSync(directoryName) ? `${directoryName} already exists. Please choose another name.` : true, }); @@ -121,11 +127,11 @@ export const reproNext = async ({ } try { - const cwd = path.isAbsolute(selectedDirectory) + const templateDestination = path.isAbsolute(selectedDirectory) ? selectedDirectory : path.join(process.cwd(), selectedDirectory); - logger.info(`🏃 Adding ${selectedConfig.name} into ${cwd}`); + logger.info(`🏃 Adding ${selectedConfig.name} into ${templateDestination}`); logger.log('📦 Downloading repro template...'); try { @@ -136,10 +142,10 @@ export const reproNext = async ({ { force: true, } - ).clone(selectedTemplate.replace('/', '-')); + ).clone(templateDestination); } catch (err) { logger.error(`🚨 Failed to download repro template: ${err.message}`); - return; + throw err; } const initMessage = init diff --git a/scripts/sandbox.ts b/scripts/sandbox.ts index d882d969adc1..81cc043dbb87 100644 --- a/scripts/sandbox.ts +++ b/scripts/sandbox.ts @@ -1,6 +1,14 @@ /* eslint-disable no-restricted-syntax, no-await-in-loop */ import path from 'path'; -import { remove, pathExists, readJSON, writeJSON, ensureSymlink, ensureDir } from 'fs-extra'; +import { + remove, + pathExists, + readJSON, + writeJSON, + ensureSymlink, + ensureDir, + existsSync, +} from 'fs-extra'; import prompts from 'prompts'; import { getOptionsOrPrompt } from './utils/options'; @@ -151,7 +159,7 @@ async function addPackageScripts({ async function readMainConfig({ cwd }: { cwd: string }) { const configDir = path.join(cwd, '.storybook'); - if (!pathExists(configDir)) { + if (!existsSync(configDir)) { throw new Error( `Unable to find the Storybook folder in "${configDir}". Are you sure it exists? Or maybe this folder uses a custom Storybook config directory?` ); diff --git a/scripts/utils/options.ts b/scripts/utils/options.ts index 2496a0500515..785f7e156b7f 100644 --- a/scripts/utils/options.ts +++ b/scripts/utils/options.ts @@ -5,6 +5,8 @@ import prompts, { Falsy, PrevCaller, PromptType } from 'prompts'; import type { PromptObject } from 'prompts'; import program from 'commander'; +import dedent from 'ts-dedent'; +import chalk from 'chalk'; import kebabCase from 'lodash/kebabCase'; // Option types @@ -125,8 +127,14 @@ export function getOptions( if (isBooleanOption(option)) return acc.option(flags, option.description, !!option.inverse); const checkStringValue = (raw: string) => { - if (!option.values.includes(raw)) - throw new Error(`Unexpected value '${raw}' for option '${key}'`); + if (!option.values.includes(raw)) { + const possibleOptions = chalk.cyan(option.values.join(', ')); + throw new Error( + dedent`Unexpected value '${chalk.yellow(raw)}' for option '${chalk.magenta(key)}'. + + These are the possible options: ${possibleOptions}\n\n` + ); + } return raw; };