259 lines
8.3 KiB
TypeScript
Raw Normal View History

2022-07-26 11:41:33 +02:00
/* eslint-disable no-console */
import { join, relative } from 'path';
2022-07-26 11:41:33 +02:00
import type { Options as ExecaOptions } from 'execa';
import pLimit from 'p-limit';
import prettyTime from 'pretty-hrtime';
import { copy, emptyDir, ensureDir, move, remove, rename, writeFile } from 'fs-extra';
import { program } from 'commander';
import { directory } from 'tempy';
import { execaCommand } from '../utils/exec';
import type { OptionValues } from '../utils/options';
import { createOptions } from '../utils/options';
2022-11-07 16:13:47 +01:00
import { allTemplates as reproTemplates } from '../../code/lib/cli/src/repro-templates';
import storybookVersions from '../../code/lib/cli/src/versions';
import { JsPackageManagerFactory } from '../../code/lib/cli/src/js-package-manager/JsPackageManagerFactory';
import { maxConcurrentTasks } from '../utils/maxConcurrentTasks';
2022-07-26 11:41:33 +02:00
2022-11-24 20:13:28 +01:00
// eslint-disable-next-line import/no-cycle
2022-07-27 11:16:08 +02:00
import { localizeYarnConfigFiles, setupYarn } from './utils/yarn';
import type { GeneratorConfig } from './utils/types';
2022-07-29 17:22:12 +02:00
import { getStackblitzUrl, renderTemplate } from './utils/template';
import type { JsPackageManager } from '../../code/lib/cli/src/js-package-manager';
2022-07-26 11:41:33 +02:00
const OUTPUT_DIRECTORY = join(__dirname, '..', '..', 'repros');
2022-07-27 16:09:29 +02:00
const BEFORE_DIR_NAME = 'before-storybook';
const AFTER_DIR_NAME = 'after-storybook';
2022-10-14 14:48:22 +11:00
const SCRIPT_TIMEOUT = 5 * 60 * 1000;
2022-07-26 11:41:33 +02:00
2022-12-02 14:51:38 +01:00
const sbInit = async (cwd: string, flags?: string[], debug?: boolean) => {
const sbCliBinaryPath = join(__dirname, `../../code/lib/cli/bin/index.js`);
console.log(`🎁 Installing storybook`);
const env = { STORYBOOK_DISABLE_TELEMETRY: 'true', STORYBOOK_REPRO_GENERATOR: 'true' };
const fullFlags = ['--yes', ...(flags || [])];
2022-12-02 14:51:38 +01:00
await runCommand(`${sbCliBinaryPath} init ${fullFlags.join(' ')}`, { cwd, env }, debug);
};
const LOCAL_REGISTRY_URL = 'http://localhost:6001';
const withLocalRegistry = async (packageManager: JsPackageManager, action: () => Promise<void>) => {
const prevUrl = packageManager.getRegistryURL();
2022-10-12 22:34:22 +02:00
let error;
try {
console.log(`📦 Configuring local registry: ${LOCAL_REGISTRY_URL}`);
packageManager.setRegistryURL(LOCAL_REGISTRY_URL);
await action();
2022-10-12 22:34:22 +02:00
} catch (e) {
error = e;
} finally {
console.log(`📦 Restoring registry: ${prevUrl}`);
packageManager.setRegistryURL(prevUrl);
2022-10-12 22:34:22 +02:00
if (error) {
// eslint-disable-next-line no-unsafe-finally
throw error;
}
}
};
2022-12-02 14:51:38 +01:00
const addStorybook = async ({
baseDir,
localRegistry,
flags,
debug,
}: {
baseDir: string;
localRegistry: boolean;
flags?: string[];
debug?: boolean;
}) => {
2022-07-27 11:16:08 +02:00
const beforeDir = join(baseDir, BEFORE_DIR_NAME);
const afterDir = join(baseDir, AFTER_DIR_NAME);
2022-07-29 17:38:06 +02:00
const tmpDir = join(baseDir, 'tmp');
2022-07-26 11:41:33 +02:00
await ensureDir(tmpDir);
await emptyDir(tmpDir);
await copy(beforeDir, tmpDir);
2022-10-10 19:34:52 -04:00
const packageManager = JsPackageManagerFactory.getPackageManager({}, tmpDir);
if (localRegistry) {
await withLocalRegistry(packageManager, async () => {
packageManager.addPackageResolutions(storybookVersions);
2022-07-26 11:41:33 +02:00
2022-12-02 14:51:38 +01:00
await sbInit(tmpDir, flags, debug);
});
} else {
2022-12-02 14:51:38 +01:00
await sbInit(tmpDir, flags, debug);
}
2022-07-26 11:41:33 +02:00
await rename(tmpDir, afterDir);
};
2022-12-02 14:51:38 +01:00
export const runCommand = async (script: string, options: ExecaOptions, debug: boolean) => {
if (debug) {
2022-07-26 13:14:22 +02:00
console.log(`Running command: ${script}`);
2022-07-26 11:41:33 +02:00
}
return execaCommand(script, {
2022-12-02 14:51:38 +01:00
stdout: debug ? 'inherit' : 'ignore',
shell: true,
...options,
});
2022-07-26 11:41:33 +02:00
};
2022-07-29 17:22:12 +02:00
const addDocumentation = async (
baseDir: string,
{ name, dirName }: { name: string; dirName: string }
) => {
const afterDir = join(baseDir, AFTER_DIR_NAME);
const stackblitzConfigPath = join(__dirname, 'templates', '.stackblitzrc');
const readmePath = join(__dirname, 'templates', 'item.ejs');
await copy(stackblitzConfigPath, join(afterDir, '.stackblitzrc'));
const stackblitzUrl = getStackblitzUrl(dirName);
const contents = await renderTemplate(readmePath, {
name,
stackblitzUrl,
});
await writeFile(join(afterDir, 'README.md'), contents);
};
const runGenerators = async (
generators: (GeneratorConfig & { dirName: string })[],
2022-12-02 14:51:38 +01:00
localRegistry = true,
debug = false
) => {
2022-07-26 11:41:33 +02:00
console.log(`🤹‍♂️ Generating repros with a concurrency of ${maxConcurrentTasks}`);
const limit = pLimit(maxConcurrentTasks);
await Promise.all(
generators.map(({ dirName, name, script, expected }) =>
2022-07-26 11:41:33 +02:00
limit(async () => {
const flags = expected.renderer === '@storybook/html' ? ['--type html'] : [];
2022-07-26 11:41:33 +02:00
const time = process.hrtime();
console.log(`🧬 generating ${name}`);
2022-07-29 17:22:12 +02:00
const baseDir = join(OUTPUT_DIRECTORY, dirName);
2022-07-27 11:16:08 +02:00
const beforeDir = join(baseDir, BEFORE_DIR_NAME);
await emptyDir(baseDir);
2022-07-26 11:41:33 +02:00
// We do the creation inside a temp dir to avoid yarn container problems
const createBaseDir = directory();
await setupYarn({ cwd: createBaseDir });
const createBeforeDir = join(createBaseDir, BEFORE_DIR_NAME);
2022-10-14 11:40:37 +11:00
// Some tools refuse to run inside an existing directory and replace the contents,
// where as others are very picky about what directories can be called. So we need to
// handle different modes of operation.
if (script.includes('{{beforeDir}}')) {
2022-12-02 13:14:16 +01:00
const scriptWithBeforeDir = script.replaceAll('{{beforeDir}}', BEFORE_DIR_NAME);
2022-12-02 14:51:38 +01:00
await runCommand(
scriptWithBeforeDir,
{
cwd: createBaseDir,
timeout: SCRIPT_TIMEOUT,
},
debug
);
2022-10-14 11:40:37 +11:00
} else {
await ensureDir(createBeforeDir);
2022-12-02 14:51:38 +01:00
await runCommand(script, { cwd: createBeforeDir, timeout: SCRIPT_TIMEOUT }, debug);
2022-10-14 11:40:37 +11:00
}
2022-07-26 13:14:22 +02:00
await localizeYarnConfigFiles(createBaseDir, createBeforeDir);
2022-07-26 11:41:33 +02:00
// Now move the created before dir into it's final location and add storybook
await move(createBeforeDir, beforeDir);
2022-07-27 11:16:08 +02:00
// Make sure there are no git projects in the folder
await remove(join(beforeDir, '.git'));
2022-12-02 14:51:38 +01:00
await addStorybook({ baseDir, localRegistry, flags, debug });
2022-07-26 11:41:33 +02:00
2022-07-29 17:22:12 +02:00
await addDocumentation(baseDir, { name, dirName });
// Remove node_modules to save space and avoid GH actions failing
// They're not uploaded to the git repros repo anyway
if (process.env.CLEANUP_REPRO_NODE_MODULES) {
console.log(`🗑️ Removing ${join(beforeDir, 'node_modules')}`);
await remove(join(beforeDir, 'node_modules'));
console.log(`🗑️ Removing ${join(baseDir, AFTER_DIR_NAME, 'node_modules')}`);
await remove(join(baseDir, AFTER_DIR_NAME, 'node_modules'));
}
2022-07-26 11:41:33 +02:00
console.log(
2022-07-29 17:22:12 +02:00
`✅ Created ${dirName} in ./${relative(
process.cwd(),
baseDir
)} successfully in ${prettyTime(process.hrtime(time))}`
2022-07-26 11:41:33 +02:00
);
})
)
);
};
export const options = createOptions({
template: {
type: 'string',
description: 'Which template would you like to create?',
values: Object.keys(reproTemplates),
},
localRegistry: {
type: 'boolean',
description: 'Generate reproduction from local registry?',
promptType: false,
},
2022-12-02 14:51:38 +01:00
debug: {
type: 'boolean',
description: 'Print all the logs to the console',
promptType: false,
},
});
2022-12-02 14:51:38 +01:00
export const generate = async ({
template,
localRegistry,
debug,
}: OptionValues<typeof options>) => {
const generatorConfigs = Object.entries(reproTemplates)
.map(([dirName, configuration]) => ({
2022-07-29 17:22:12 +02:00
dirName,
...configuration,
2022-07-26 11:41:33 +02:00
}))
.filter(({ dirName }) => {
if (template) {
return dirName === template;
}
return true;
});
2022-07-26 11:41:33 +02:00
2022-12-02 14:51:38 +01:00
await runGenerators(generatorConfigs, localRegistry, debug);
};
2022-07-26 11:41:33 +02:00
if (require.main === module) {
program
.description('Create a reproduction from a set of possible templates')
2022-12-02 14:51:38 +01:00
.option('--template <template>', 'Create a single template')
.option('--debug', 'Print all the logs to the console')
.option('--local-registry', 'Use local registry', false)
.action((optionValues) => {
generate(optionValues)
.catch((e) => {
console.trace(e);
process.exit(1);
})
.then(() => {
// FIXME: Kill dangling processes. For some reason in CI,
// the abort signal gets executed but the child process kill
// does not succeed?!?
process.exit(0);
});
})
.parse(process.argv);
}