Merge pull request #14888 from storybookjs/cli-repro-refinements

CLI: Repro refinements per feedback
This commit is contained in:
Michael Shilman 2021-05-13 12:37:38 +08:00 committed by GitHub
commit 532293dda0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 95 deletions

View File

@ -51,6 +51,7 @@
"@storybook/codemod": "6.3.0-alpha.24",
"@storybook/node-logger": "6.3.0-alpha.24",
"@storybook/semver": "^7.3.2",
"boxen": "^4.2.0",
"chalk": "^4.1.0",
"commander": "^6.2.1",
"core-js": "^3.8.2",
@ -69,6 +70,7 @@
"read-pkg-up": "^7.0.1",
"shelljs": "^0.8.4",
"strip-json-comments": "^3.0.1",
"ts-dedent": "^2.0.0",
"update-notifier": "^5.0.1"
},
"devDependencies": {

View File

@ -1,7 +1,7 @@
/* eslint-disable no-irregular-whitespace */
import path from 'path';
import { writeJSON } from 'fs-extra';
import shell, { ExecOptions } from 'shelljs';
import chalk from 'chalk';
const logger = console;
@ -39,39 +39,50 @@ export interface Options extends Parameters {
pnp: boolean;
}
export const exec = async (command: string, options: ExecOptions = {}) =>
new Promise((resolve, reject) => {
shell.exec(command, options, (code) => {
export const exec = async (
command: string,
options: ExecOptions = {},
{ startMessage, errorMessage }: { startMessage?: string; errorMessage?: string } = {}
) => {
if (startMessage) {
logger.info(startMessage);
}
logger.debug(command);
return new Promise((resolve, reject) => {
const defaultOptions: ExecOptions = {
silent: true,
};
shell.exec(command, { ...defaultOptions, ...options }, (code, stdout, stderr) => {
if (code === 0) {
resolve(undefined);
} else {
reject(new Error(`command exited with code: ${code}`));
logger.error(chalk.red(`An error occurred while executing: \`${command}\``));
logger.error(`Command output was:${chalk.yellow(`\n${stdout}\n${stderr}`)}`);
if (errorMessage) {
logger.error(errorMessage);
}
reject(new Error(`command exited with code: ${code}: `));
}
});
});
};
const installYarn2 = async ({ cwd, pnp }: Options) => {
const commands = [
const command = [
`yarn set version berry`,
`yarn config set enableGlobalCache true`,
`yarn config set nodeLinker ${pnp ? 'pnp' : 'node-modules'}`,
];
].join(' && ');
const command = commands.join(' && ');
logger.info(`🧶Installing Yarn 2`);
logger.debug(command);
try {
await exec(command, { cwd });
} catch (e) {
logger.error(`🚨Installing Yarn 2 failed`);
throw e;
}
await exec(
command,
{ cwd },
{ startMessage: `🧶 Installing Yarn 2`, errorMessage: `🚨 Installing Yarn 2 failed` }
);
};
const configureYarn2ForE2E = async ({ cwd }: Options) => {
const commands = [
const command = [
// ⚠️ Need to set registry because Yarn 2 is not using the conf of Yarn 1 (URL is hardcoded in CircleCI config.yml)
`yarn config set npmScopes --json '{ "storybook": { "npmRegistryServer": "http://localhost:6000/" } }'`,
// Some required magic to be able to fetch deps from local registry
@ -82,72 +93,68 @@ const configureYarn2ForE2E = async ({ cwd }: Options) => {
`yarn config set enableImmutableInstalls false`,
// Discard all YN0013 - FETCH_NOT_CACHED messages
`yarn config set logFilters --json '[ { "code": "YN0013", "level": "discard" } ]'`,
];
].join(' && ');
const command = commands.join(' && ');
logger.info(`🎛Configuring Yarn 2`);
logger.debug(command);
try {
await exec(command, { cwd });
} catch (e) {
logger.error(`🚨Configuring Yarn 2 failed`);
throw e;
}
await exec(
command,
{ cwd },
{ startMessage: `🎛 Configuring Yarn 2`, errorMessage: `🚨 Configuring Yarn 2 failed` }
);
};
const generate = async ({ cwd, name, appName, version, generator }: Options) => {
const command = generator.replace(/{{appName}}/g, appName).replace(/{{version}}/g, version);
logger.info(`🏗Bootstrapping ${name} project`);
logger.debug(command);
try {
await exec(command, { cwd });
} catch (e) {
logger.error(`🚨Bootstrapping ${name} failed`);
throw e;
}
await exec(
command,
{ cwd },
{
startMessage: `🏗 Bootstrapping ${name} project (this might take a few minutes)`,
errorMessage: `🚨 Bootstrapping ${name} failed`,
}
);
};
const initStorybook = async ({ cwd, autoDetect = true, name, e2e }: Options) => {
logger.info(`🎨Initializing Storybook with @storybook/cli`);
try {
const type = autoDetect ? '' : `--type ${name}`;
const linkable = e2e ? '' : '--linkable';
const sbCLICommand = useLocalSbCli
? `node ${path.join(__dirname, '../../esm/generate')}`
: `yarn dlx -p @storybook/cli sb`;
const type = autoDetect ? '' : `--type ${name}`;
const linkable = e2e ? '' : '--linkable';
const sbCLICommand = useLocalSbCli
? `node ${path.join(__dirname, '../../esm/generate')}`
: `yarn dlx -p @storybook/cli sb`;
await exec(`${sbCLICommand} init --yes ${type} ${linkable}`, { cwd });
} catch (e) {
logger.error(`🚨Storybook initialization failed`);
throw e;
}
const command = `${sbCLICommand} init --yes ${type} ${linkable}`;
await exec(
command,
{ cwd },
{
startMessage: `🎨 Initializing Storybook with @storybook/cli`,
errorMessage: `🚨 Storybook initialization failed`,
}
);
};
const addRequiredDeps = async ({ cwd, additionalDeps }: Options) => {
logger.info(`🌍Adding needed deps & installing all deps`);
try {
// Remove any lockfile generated without Yarn 2
shell.rm(path.join(cwd, 'package-lock.json'), path.join(cwd, 'yarn.lock'));
if (additionalDeps && additionalDeps.length > 0) {
await exec(`yarn add -D ${additionalDeps.join(' ')}`, {
cwd,
});
} else {
await exec(`yarn install`, {
cwd,
});
// Remove any lockfile generated without Yarn 2
shell.rm(path.join(cwd, 'package-lock.json'), path.join(cwd, 'yarn.lock'));
const command =
additionalDeps && additionalDeps.length > 0
? `yarn add -D ${additionalDeps.join(' ')}`
: `yarn install`;
await exec(
command,
{ cwd },
{
startMessage: `🌍 Adding needed deps & installing all deps`,
errorMessage: `🚨 Dependencies installation failed`,
}
} catch (e) {
logger.error(`🚨Dependencies installation failed`);
throw e;
}
);
};
const addTypescript = async ({ cwd }: Options) => {
logger.info(`👮🏻 Adding typescript and tsconfig.json`);
logger.info(`👮 Adding typescript and tsconfig.json`);
try {
await exec(`yarn add -D typescript@latest`, { cwd });
const tsConfig = {
@ -163,7 +170,7 @@ const addTypescript = async ({ cwd }: Options) => {
const tsConfigJsonPath = path.resolve(cwd, 'tsconfig.json');
await writeJSON(tsConfigJsonPath, tsConfig, { encoding: 'utf8', spaces: 2 });
} catch (e) {
logger.error(`🚨Creating tsconfig.json failed`);
logger.error(`🚨 Creating tsconfig.json failed`);
throw e;
}
};
@ -196,20 +203,16 @@ export const createAndInit = async (
};
logger.log();
logger.info(`🏃‍♀️ Starting for ${name} ${version}`);
logger.info(`🏃 Starting for ${name} ${version}`);
logger.log();
logger.debug(options);
logger.log();
console.log({ creationPath: options.creationPath });
await doTask(generate, { ...options, cwd: options.creationPath });
await doTask(installYarn2, options);
if (e2e) {
await doTask(configureYarn2ForE2E, options);
}
await doTask(configureYarn2ForE2E, options, e2e);
await doTask(addTypescript, options, !!options.typescript);
await doTask(addRequiredDeps, options);

View File

@ -1,10 +1,15 @@
import prompts from 'prompts';
import { logger } from '@storybook/node-logger';
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import dedent from 'ts-dedent';
import { createAndInit, Parameters, exec } from './repro-generators/scripts';
import * as configs from './repro-generators/configs';
import { SupportedFrameworks } from './project_types';
const logger = console;
interface ReproOptions {
outputDirectory: string;
framework?: SupportedFrameworks;
@ -35,7 +40,7 @@ export const repro = async ({
pnp,
}: ReproOptions) => {
if (list) {
logger.info('Available templates');
logger.info('🌈 Available templates');
Object.entries(FRAMEWORKS).forEach(([fmwrk, templates]) => {
logger.info(fmwrk);
templates.forEach((t) => logger.info(`- ${t.name}`));
@ -46,26 +51,13 @@ export const repro = async ({
return;
}
let selectedDirectory = outputDirectory;
if (!selectedDirectory) {
const { directory } = await prompts({
type: 'text',
message: 'Enter the output directory',
name: 'directory',
});
selectedDirectory = directory;
// if (fs.existsSync(selectedDirectory)) {
// throw new Error(`Repro: ${selectedDirectory} already exists`);
// }
}
let selectedTemplate = template;
let selectedFramework = framework;
if (!selectedTemplate && !generator) {
if (!selectedFramework) {
const { framework: frameworkOpt } = await prompts({
type: 'select',
message: 'Select the repro framework',
message: '🌈 Select the repro framework',
name: 'framework',
choices: Object.keys(FRAMEWORKS).map((f) => ({ title: f, value: f })),
});
@ -74,7 +66,7 @@ export const repro = async ({
selectedTemplate = (
await prompts({
type: 'select',
message: 'Select the repro base template',
message: '📝 Select the repro base template',
name: 'template',
choices: FRAMEWORKS[selectedFramework as SupportedFrameworks].map((f) => ({
title: f.name,
@ -93,7 +85,20 @@ export const repro = async ({
};
if (!selectedConfig) {
throw new Error('Repro: please specify a valid template type');
throw new Error('🚨 Repro: please specify a valid template type');
}
let selectedDirectory = outputDirectory;
if (!selectedDirectory) {
const { directory } = await prompts({
type: 'text',
message: 'Enter the output directory',
name: 'directory',
});
selectedDirectory = directory;
if (fs.existsSync(selectedDirectory)) {
throw new Error(`🚨 Repro: ${selectedDirectory} already exists`);
}
}
try {
@ -101,7 +106,7 @@ export const repro = async ({
? selectedDirectory
: path.join(process.cwd(), selectedDirectory);
logger.info(`Running ${selectedTemplate} into ${cwd}`);
logger.info(`🏃 Running ${selectedTemplate} into ${cwd}`);
await createAndInit(cwd, selectedConfig, {
e2e: !!e2e,
@ -111,8 +116,28 @@ export const repro = async ({
if (!e2e) {
await initGitRepo(cwd);
}
logger.info(
boxen(
dedent`
🎉 Your Storybook reproduction project is ready to use! 🎉
${chalk.yellow(`cd ${selectedDirectory}`)}
${chalk.yellow(`yarn storybook`)}
Once you've recreated the problem you're experiencing, please:
1. Document any additional steps in ${chalk.cyan('README.md')}
2. Publish the repository to github
3. Link to the repro repository in your issue
Having a clean repro helps us solve your issue faster! 🙏
`.trim(),
{ borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any
)
);
} catch (error) {
logger.error('Failed to create repro');
logger.error('🚨 Failed to create repro');
}
};

View File

@ -6347,6 +6347,7 @@ __metadata:
"@types/semver": ^7.3.4
"@types/shelljs": ^0.8.7
"@types/update-notifier": ^5.0.0
boxen: ^4.2.0
chalk: ^4.1.0
commander: ^6.2.1
core-js: ^3.8.2
@ -6365,6 +6366,7 @@ __metadata:
read-pkg-up: ^7.0.1
shelljs: ^0.8.4
strip-json-comments: ^3.1.1
ts-dedent: ^2.0.0
update-notifier: ^4.1.3
peerDependencies:
jest: "*"