lib/cli/src/repro-generators/scripts.ts
import path from 'path';
import { writeJSON } from 'fs-extra';
import shell, { ExecOptions } from 'shelljs';
import chalk from 'chalk';
import { cra, cra_typescript } from './configs';
const logger = console;
export interface Parameters {
/** E2E configuration name */
name: string;
/** framework version */
version: string;
/** CLI to bootstrap the project */
generator: string;
/** Use storybook framework detection */
autoDetect?: boolean;
/** Pre-build hook */
preBuildCommand?: string;
/** When cli complains when folder already exists */
ensureDir?: boolean;
/** Dependencies to add before building Storybook */
additionalDeps?: string[];
/** Add typescript dependency and creates a tsconfig.json file */
typescript?: boolean;
}
interface Configuration {
e2e: boolean;
pnp: boolean;
}
const useLocalSbCli = true;
export interface Options extends Parameters {
appName: string;
creationPath: string;
cwd?: string;
e2e: boolean;
pnp: boolean;
}
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,
};
const child = shell.exec(command, { ...defaultOptions, ...options, async: true });
child.stderr.pipe(process.stderr);
child.stdout.pipe(process.stdout);
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}: `));
}
});
});
};
const installYarn2 = async ({ cwd, pnp, name }: Options) => {
const command = [
`yarn set version berry`,
`yarn config set enableGlobalCache true`,
`yarn config set nodeLinker ${pnp ? 'pnp' : 'node-modules'}`,
];
// FIXME: Some dependencies used by CRA aren't listed in its package.json
// Next line is a hack to remove as soon as CRA will have added these missing deps
// for details see https://github.com/facebook/create-react-app/pull/11751
if ([cra.name, cra_typescript.name].includes(name)) {
command.push(
`yarn config set packageExtensions --json '{ "babel-preset-react-app@10.0.x": { "dependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.16.0" } } }'`
);
}
await exec(
command.join(' && '),
{ cwd },
{ startMessage: `๐งถ Installing Yarn 2`, errorMessage: `๐จ Installing Yarn 2 failed` }
);
};
const configureYarn2ForE2E = async ({ cwd }: Options) => {
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
`yarn config set unsafeHttpWhitelist --json '["localhost"]'`,
// Disable fallback mode to make sure everything is required correctly
`yarn config set pnpFallbackMode none`,
// We need to be able to update lockfile when bootstrapping the examples
`yarn config set enableImmutableInstalls false`,
// Discard all YN0013 - FETCH_NOT_CACHED messages
`yarn config set logFilters --json '[ { "code": "YN0013", "level": "discard" } ]'`,
].join(' && ');
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);
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) => {
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 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) => {
// Remove any lockfile generated without Yarn 2
shell.rm('-f', 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`,
}
);
};
const addTypescript = async ({ cwd }: Options) => {
logger.info(`๐ฎ Adding typescript and tsconfig.json`);
try {
await exec(`yarn add -D typescript@latest`, { cwd });
const tsConfig = {
compilerOptions: {
baseUrl: '.',
esModuleInterop: true,
jsx: 'preserve',
skipLibCheck: true,
strict: true,
},
include: ['src/*'],
};
const tsConfigJsonPath = path.resolve(cwd, 'tsconfig.json');
await writeJSON(tsConfigJsonPath, tsConfig, { encoding: 'utf8', spaces: 2 });
} catch (e) {
logger.error(`๐จ Creating tsconfig.json failed`);
throw e;
}
};
const doTask = async (
task: (options: Options) => Promise<void>,
options: Options,
condition = true
) => {
if (condition) {
await task(options);
logger.log();
}
};
export const createAndInit = async (
cwd: string,
{ name, version, ...rest }: Parameters,
{ e2e, pnp }: Configuration
) => {
const options: Options = {
name,
version,
appName: path.basename(cwd),
creationPath: path.join(cwd, '..'),
cwd,
e2e,
pnp,
...rest,
};
logger.log();
logger.info(`๐ Starting for ${name} ${version}`);
logger.log();
await doTask(generate, { ...options, cwd: options.creationPath });
await doTask(installYarn2, options);
await doTask(configureYarn2ForE2E, options, e2e);
await doTask(addTypescript, options, !!options.typescript);
await doTask(addRequiredDeps, options);
await doTask(initStorybook, options);
};