mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-19 05:02:40 +08:00
Merge pull request #14888 from storybookjs/cli-repro-refinements
CLI: Repro refinements per feedback
This commit is contained in:
commit
532293dda0
@ -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": {
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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: "*"
|
||||
|
Loading…
x
Reference in New Issue
Block a user