/* eslint-disable no-await-in-loop, no-restricted-syntax */ import { getJunitXml } from 'junit-xml'; import { outputFile, existsSync, readFile } from 'fs-extra'; import { join, resolve } from 'path'; import { createOptions, getOptionsOrPrompt } from './utils/options'; import { bootstrap } from './tasks/bootstrap'; import { publish } from './tasks/publish'; import { create } from './tasks/create'; import { smokeTest } from './tasks/smoke-test'; import { build } from './tasks/build'; import { testRunner } from './tasks/test-runner'; import { chromatic } from './tasks/chromatic'; import { e2eTests } from './tasks/e2e-tests'; import TEMPLATES from '../code/lib/cli/src/repro-templates'; const sandboxDir = resolve(__dirname, '../sandbox'); const junitDir = resolve(__dirname, '../code/test-results'); export type TemplateKey = keyof typeof TEMPLATES; export type Template = typeof TEMPLATES[TemplateKey]; export type Path = string; export type TemplateDetails = { template: Template; sandboxDir: Path; builtSandboxDir: Path; junitFilename: Path; }; type MaybePromise = T | Promise; export type Task = { /** * Which tasks run before this task */ before?: TaskKey[]; /** * Is this task already "ready", and potentially not required? */ ready: (templateKey: TemplateKey, details: TemplateDetails) => MaybePromise; /** * Run the task */ run: (templateKey: TemplateKey, details: TemplateDetails) => MaybePromise; /** * Does this task handle its own junit results? */ junit?: boolean; }; export const tasks = { bootstrap, publish, create, 'smoke-test': smokeTest, build, 'test-runner': testRunner, chromatic, 'e2e-tests': e2eTests, }; type TaskKey = keyof typeof tasks; export const options = createOptions({ task: { type: 'string', description: 'What task are you performing (corresponds to CI job)?', values: Object.keys(tasks) as TaskKey[], required: true, }, template: { type: 'string', description: 'What template are you running against?', values: Object.keys(TEMPLATES) as TemplateKey[], required: true, }, force: { type: 'boolean', description: 'The task must run, it is an error if it is already ready.', }, before: { type: 'boolean', description: 'Run any required dependencies of the task?', inverse: true, }, junit: { type: 'boolean', description: 'Store results in junit format?', }, }); const logger = console; function getJunitFilename(taskKey: TaskKey) { return join(junitDir, `${taskKey}.xml`); } async function writeJunitXml(taskKey: TaskKey, templateKey: TemplateKey, start: Date, err?: Error) { const name = `${taskKey} - ${templateKey}`; const time = (Date.now() - +start) / 1000; const testCase = { name, assertions: 1, time, ...(err && { errors: [err] }) }; const suite = { name, timestamp: start, time, testCases: [testCase] }; const junitXml = getJunitXml({ time, name, suites: [suite] }); const path = getJunitFilename(taskKey); await outputFile(path, junitXml); logger.log(`Test results written to ${resolve(path)}`); } async function runTask( taskKey: TaskKey, templateKey: TemplateKey, { mustNotBeReady, mustBeReady, before, junit, }: { mustNotBeReady: boolean; mustBeReady: boolean; before: boolean; junit: boolean } ) { const task = tasks[taskKey]; const template = TEMPLATES[templateKey]; const templateSandboxDir = join(sandboxDir, templateKey.replace('/', '-')); const details = { template, sandboxDir: templateSandboxDir, builtSandboxDir: join(templateSandboxDir, 'storybook-static'), junitFilename: junit && getJunitFilename(taskKey), }; if (await task.ready(templateKey, details)) { if (mustNotBeReady) throw new Error(`❌ ${taskKey} task has already run, this is unexpected!`); logger.debug(`✅ ${taskKey} task not required!`); return; } if (mustBeReady) { throw new Error(`❌ ${taskKey} task has not already run, this is unexpected!`); } if (task.before?.length > 0) { for (const beforeKey of task.before) { await runTask(beforeKey, templateKey, { mustNotBeReady: false, mustBeReady: !before, before, junit: false, // never store junit results for dependent tasks }); } } const start = new Date(); try { await task.run(templateKey, details); if (junit && !task.junit) await writeJunitXml(taskKey, templateKey, start); } catch (err) { if (junit && !task.junit) await writeJunitXml(taskKey, templateKey, start, err); throw err; } finally { const { junitFilename } = details; if (existsSync(junitFilename)) { const junitXml = await (await readFile(junitFilename)).toString(); const prefixedXml = junitXml.replace(/classname="(.*)"/g, `classname="${templateKey} $1"`); await outputFile(junitFilename, prefixedXml); } } } async function run() { const { task: taskKey, template: templateKey, force, before, junit, } = await getOptionsOrPrompt('yarn task', options); return runTask(taskKey, templateKey, { mustBeReady: false, mustNotBeReady: force, before, junit, }); } if (require.main === module) { run() .then(() => process.exit(0)) .catch((err) => { logger.error(); logger.error(err.message); process.exit(1); }); }