2022-07-26 11:41:33 +02:00
|
|
|
/* eslint-disable no-console */
|
2022-08-14 17:11:47 +08:00
|
|
|
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';
|
2022-10-18 12:50:49 +02:00
|
|
|
import { copy, emptyDir, ensureDir, move, remove, rename, writeFile } from 'fs-extra';
|
2022-08-16 12:16:39 +02:00
|
|
|
import { program } from 'commander';
|
2022-10-12 11:57:04 +11:00
|
|
|
import { directory } from 'tempy';
|
2022-11-09 15:27:56 +11:00
|
|
|
import { execaCommand } from '../utils/exec';
|
2022-08-23 08:22:16 +08:00
|
|
|
|
2022-11-15 20:01:19 +11:00
|
|
|
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';
|
2022-08-16 12:16:39 +02:00
|
|
|
import storybookVersions from '../../code/lib/cli/src/versions';
|
|
|
|
import { JsPackageManagerFactory } from '../../code/lib/cli/src/js-package-manager/JsPackageManagerFactory';
|
2022-08-14 17:11:47 +08:00
|
|
|
|
2022-10-04 20:54:40 +11:00
|
|
|
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';
|
2022-11-02 17:57:31 +01:00
|
|
|
import type { GeneratorConfig } from './utils/types';
|
2022-07-29 17:22:12 +02:00
|
|
|
import { getStackblitzUrl, renderTemplate } from './utils/template';
|
2022-11-02 17:57:31 +01:00
|
|
|
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) => {
|
2022-08-17 18:32:31 +08:00
|
|
|
const sbCliBinaryPath = join(__dirname, `../../code/lib/cli/bin/index.js`);
|
|
|
|
console.log(`🎁 Installing storybook`);
|
2022-10-13 11:27:27 -06:00
|
|
|
const env = { STORYBOOK_DISABLE_TELEMETRY: 'true', STORYBOOK_REPRO_GENERATOR: 'true' };
|
2022-10-06 17:32:37 +02:00
|
|
|
const fullFlags = ['--yes', ...(flags || [])];
|
2022-12-02 14:51:38 +01:00
|
|
|
await runCommand(`${sbCliBinaryPath} init ${fullFlags.join(' ')}`, { cwd, env }, debug);
|
2022-08-17 18:32:31 +08:00
|
|
|
};
|
|
|
|
|
2022-08-22 17:40:30 +08:00
|
|
|
const LOCAL_REGISTRY_URL = 'http://localhost:6001';
|
2022-08-17 18:32:31 +08:00
|
|
|
const withLocalRegistry = async (packageManager: JsPackageManager, action: () => Promise<void>) => {
|
|
|
|
const prevUrl = packageManager.getRegistryURL();
|
2022-10-12 22:34:22 +02:00
|
|
|
let error;
|
2022-08-17 18:32:31 +08:00
|
|
|
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;
|
2022-08-17 18:32:31 +08:00
|
|
|
} 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-08-17 18:32:31 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
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);
|
2022-08-17 18:32:31 +08:00
|
|
|
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);
|
2022-08-17 18:32:31 +08:00
|
|
|
});
|
|
|
|
} else {
|
2022-12-02 14:51:38 +01:00
|
|
|
await sbInit(tmpDir, flags, debug);
|
2022-08-17 18:32:31 +08:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2022-11-09 15:27:56 +11:00
|
|
|
return execaCommand(script, {
|
2022-12-02 14:51:38 +01:00
|
|
|
stdout: debug ? 'inherit' : 'ignore',
|
2022-11-09 15:27:56 +11:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2022-08-16 12:16:39 +02:00
|
|
|
const runGenerators = async (
|
|
|
|
generators: (GeneratorConfig & { dirName: string })[],
|
2022-12-02 14:51:38 +01:00
|
|
|
localRegistry = true,
|
|
|
|
debug = false
|
2022-08-16 12:16:39 +02:00
|
|
|
) => {
|
2022-07-26 11:41:33 +02:00
|
|
|
console.log(`🤹♂️ Generating repros with a concurrency of ${maxConcurrentTasks}`);
|
|
|
|
|
|
|
|
const limit = pLimit(maxConcurrentTasks);
|
|
|
|
|
2022-08-23 17:45:00 +08:00
|
|
|
await Promise.all(
|
2022-10-06 17:32:37 +02:00
|
|
|
generators.map(({ dirName, name, script, expected }) =>
|
2022-07-26 11:41:33 +02:00
|
|
|
limit(async () => {
|
2022-10-06 17:32:37 +02:00
|
|
|
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
|
|
|
|
2022-10-12 11:57:04 +11: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
|
|
|
|
2022-10-12 11:57:04 +11:00
|
|
|
await localizeYarnConfigFiles(createBaseDir, createBeforeDir);
|
2022-07-26 11:41:33 +02:00
|
|
|
|
2022-10-12 11:57:04 +11: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
|
|
|
|
2022-10-18 12:50:49 +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 });
|
|
|
|
|
2022-11-02 17:57:31 +01:00
|
|
|
// Remove node_modules to save space and avoid GH actions failing
|
|
|
|
// They're not uploaded to the git repros repo anyway
|
2022-11-02 19:52:51 +01:00
|
|
|
if (process.env.CLEANUP_REPRO_NODE_MODULES) {
|
2022-11-18 12:24:32 +01:00
|
|
|
console.log(`🗑️ Removing ${join(beforeDir, 'node_modules')}`);
|
2022-11-02 17:57:31 +01:00
|
|
|
await remove(join(beforeDir, 'node_modules'));
|
2022-11-18 12:24:32 +01:00
|
|
|
console.log(`🗑️ Removing ${join(baseDir, AFTER_DIR_NAME, 'node_modules')}`);
|
|
|
|
await remove(join(baseDir, AFTER_DIR_NAME, 'node_modules'));
|
2022-11-02 17:57:31 +01:00
|
|
|
}
|
|
|
|
|
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
|
|
|
);
|
|
|
|
})
|
|
|
|
)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2022-11-15 20:01:19 +11: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-11-15 20:01:19 +11:00
|
|
|
});
|
|
|
|
|
2022-12-02 14:51:38 +01:00
|
|
|
export const generate = async ({
|
|
|
|
template,
|
|
|
|
localRegistry,
|
|
|
|
debug,
|
|
|
|
}: OptionValues<typeof options>) => {
|
2022-08-16 12:16:39 +02:00
|
|
|
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
|
|
|
}))
|
2022-08-16 12:16:39 +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-08-16 12:16:39 +02:00
|
|
|
};
|
2022-07-26 11:41:33 +02:00
|
|
|
|
2022-11-15 20:01:19 +11: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')
|
2022-11-15 20:01:19 +11:00
|
|
|
.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);
|
|
|
|
}
|