diff --git a/lib/builder-webpack4/src/index.ts b/lib/builder-webpack4/src/index.ts index 2462012e12bf..db89a415581d 100644 --- a/lib/builder-webpack4/src/index.ts +++ b/lib/builder-webpack4/src/index.ts @@ -11,6 +11,19 @@ let compilation: ReturnType; let reject: (reason?: any) => void; type WebpackBuilder = Builder; +type Unpromise> = T extends Promise ? U : never; + +type BuilderStartOptions = Partial['0']>; +type BuilderStartResult = Unpromise>; +type StarterFunction = ( + options: BuilderStartOptions +) => AsyncGenerator; + +type BuilderBuildOptions = Partial['0']>; +type BuilderBuildResult = Unpromise>; +type BuilderFunction = ( + options: BuilderBuildOptions +) => AsyncGenerator; export const executor = { get: async (options: Options) => { @@ -48,11 +61,54 @@ export const makeStatsFromError: (err: string) => Stats = (err) => toJson: () => ({ warnings: [] as any[], errors: [err] }), } as any); -export const start: WebpackBuilder['start'] = async ({ startTime, options, router }) => { +let asyncIterator: ReturnType | ReturnType; + +export const bail: WebpackBuilder['bail'] = async () => { + if (asyncIterator) { + try { + // we tell the builder (that started) to stop ASAP and wait + await asyncIterator.throw(new Error()); + } catch (e) { + // + } + } + if (reject) { + reject(); + } + // we wait for the compiler to finish it's work, so it's command-line output doesn't interfere + return new Promise((res, rej) => { + if (process && compilation) { + try { + compilation.close(() => res()); + logger.warn('Force closed preview build'); + } catch (err) { + logger.warn('Unable to close preview build!'); + res(); + } + } else { + res(); + } + }); +}; + +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. manager builder + * + * I am sorry for making you read about generators today :') + */ +const starter: StarterFunction = async function* starterGeneratorFn({ + startTime, + options, + router, +}) { const webpackInstance = await executor.get(options); + yield; const config = await getConfig(options); + yield; const compiler = webpackInstance(config); + if (!compiler) { const err = `${config.name}: missing webpack compiler at runtime!`; logger.error(err); @@ -64,6 +120,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route } const { handler, modulesCount } = await useProgressReporting(router, startTime, options); + yield; new ProgressPlugin({ handler, modulesCount }).apply(compiler); const middlewareOptions: Parameters[1] = { @@ -86,6 +143,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route waitUntilValid(ready); reject = stop; }); + yield; if (!stats) { throw new Error('no stats after building preview'); @@ -102,26 +160,18 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route }; }; -export const bail: WebpackBuilder['bail'] = (e: Error) => { - if (reject) { - reject(); - } - if (process) { - try { - compilation.close(); - logger.warn('Force closed preview build'); - } catch (err) { - logger.warn('Unable to close preview build!'); - } - } - throw e; -}; - -export const build: WebpackBuilder['build'] = async ({ options, startTime }) => { +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. manager builder + * + * I am sorry for making you read about generators today :') + */ +const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, options }) { const webpackInstance = await executor.get(options); - + yield; logger.info('=> Compiling preview..'); const config = await getConfig(options); + yield; const compiler = webpackInstance(config); if (!compiler) { @@ -129,8 +179,9 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => logger.error(err); return Promise.resolve(makeStatsFromError(err)); } + yield; - return new Promise((succeed, fail) => { + return new Promise((succeed, fail) => { compiler.run((error, stats) => { if (error || !stats || stats.hasErrors()) { logger.error('=> Failed to build the preview'); @@ -142,11 +193,22 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => } if (stats && (stats.hasErrors() || stats.hasWarnings())) { - const { warnings, errors } = stats.toJson(config.stats); + const { warnings = [], errors = [] } = stats.toJson( + typeof config.stats === 'string' + ? config.stats + : { + warnings: true, + errors: true, + ...(config.stats as Stats.ToStringOptionsObject), + } + ); errors.forEach((e: string) => logger.error(e)); warnings.forEach((e: string) => logger.error(e)); - return fail(stats); + + return options.debugWebpack + ? fail(stats) + : fail(new Error('=> Webpack failed, learn more with --debug-webpack')); } } @@ -155,10 +217,34 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => stats.toJson(config.stats).warnings.forEach((e: string) => logger.warn(e)); } - return succeed(stats); + return succeed(stats as webpackReal.Stats); }); }); }; +export const start = async (options: BuilderStartOptions) => { + asyncIterator = starter(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; +}; + +export const build = async (options: BuilderStartOptions) => { + asyncIterator = builder(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; +}; + export const corePresets = [require.resolve('./presets/preview-preset.js')]; export const overridePresets = [require.resolve('./presets/custom-webpack-preset.js')]; diff --git a/lib/builder-webpack5/src/index.ts b/lib/builder-webpack5/src/index.ts index 522c69c060d8..2d2a84c4646e 100644 --- a/lib/builder-webpack5/src/index.ts +++ b/lib/builder-webpack5/src/index.ts @@ -1,4 +1,4 @@ -import webpack, { Stats, Configuration, ProgressPlugin } from 'webpack'; +import webpack, { Stats, Configuration, ProgressPlugin, StatsOptions } from 'webpack'; import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; import { logger } from '@storybook/node-logger'; @@ -9,6 +9,30 @@ let compilation: ReturnType; let reject: (reason?: any) => void; type WebpackBuilder = Builder; +type Unpromise> = T extends Promise ? U : never; + +type BuilderStartOptions = Partial['0']>; +type BuilderStartResult = Unpromise>; +type StarterFunction = ( + options: BuilderStartOptions +) => AsyncGenerator; + +type BuilderBuildOptions = Partial['0']>; +type BuilderBuildResult = Unpromise>; +type BuilderFunction = ( + options: BuilderBuildOptions +) => AsyncGenerator; + +export const executor = { + get: async (options: Options) => { + const version = ((await options.presets.apply('webpackVersion')) || '5') as string; + const webpackInstance = + (await options.presets.apply<{ default: typeof webpack }>('webpackInstance'))?.default || + webpack; + checkWebpackVersion({ version }, '5', 'builder-webpack5'); + return webpackInstance; + }, +}; export const getConfig: WebpackBuilder['getConfig'] = async (options) => { const { presets } = options; @@ -28,22 +52,55 @@ export const getConfig: WebpackBuilder['getConfig'] = async (options) => { ) as any; }; -export const executor = { - get: async (options: Options) => { - const version = ((await options.presets.apply('webpackVersion')) || '5') as string; - const webpackInstance = - (await options.presets.apply<{ default: typeof webpack }>('webpackInstance'))?.default || - webpack; - checkWebpackVersion({ version }, '5', 'builder-webpack5'); - return webpackInstance; - }, +let asyncIterator: ReturnType | ReturnType; + +export const bail: WebpackBuilder['bail'] = async () => { + if (asyncIterator) { + try { + // we tell the builder (that started) to stop ASAP and wait + await asyncIterator.throw(new Error()); + } catch (e) { + // + } + } + + if (reject) { + reject(); + } + // we wait for the compiler to finish it's work, so it's command-line output doesn't interfere + return new Promise((res, rej) => { + if (process && compilation) { + try { + compilation.close(() => res()); + logger.warn('Force closed preview build'); + } catch (err) { + logger.warn('Unable to close preview build!'); + res(); + } + } else { + res(); + } + }); }; -export const start: WebpackBuilder['start'] = async ({ startTime, options, router }) => { +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. preview builder + * + * I am sorry for making you read about generators today :') + */ +const starter: StarterFunction = async function* starterGeneratorFn({ + startTime, + options, + router, +}) { const webpackInstance = await executor.get(options); + yield; const config = await getConfig(options); + yield; const compiler = webpackInstance(config); + if (!compiler) { const err = `${config.name}: missing webpack compiler at runtime!`; logger.error(err); @@ -59,6 +116,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route } const { handler, modulesCount } = await useProgressReporting(router, startTime, options); + yield; new ProgressPlugin({ handler, modulesCount }).apply(compiler); const middlewareOptions: Parameters[1] = { @@ -75,6 +133,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route compilation.waitUntilValid(ready); reject = stop; }); + yield; if (!stats) { throw new Error('no stats after building preview'); @@ -91,28 +150,20 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route }; }; -export const bail: WebpackBuilder['bail'] = (e: Error) => { - if (reject) { - reject(); - } - if (process) { - try { - compilation.close(); - logger.warn('Force closed preview build'); - } catch (err) { - logger.warn('Unable to close preview build!'); - } - } - throw e; -}; - -export const build: WebpackBuilder['build'] = async ({ options, startTime }) => { +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. manager builder + * + * I am sorry for making you read about generators today :') + */ +const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, options }) { const webpackInstance = await executor.get(options); - + yield; logger.info('=> Compiling preview..'); const config = await getConfig(options); + yield; - return new Promise((succeed, fail) => { + return new Promise((succeed, fail) => { const compiler = webpackInstance(config); compiler.run((error, stats) => { @@ -129,7 +180,15 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => } if (stats && (stats.hasErrors() || stats.hasWarnings())) { - const { warnings = [], errors = [] } = stats.toJson({ warnings: true, errors: true }); + const { warnings = [], errors = [] } = stats.toJson( + typeof config.stats === 'string' + ? config.stats + : { + warnings: true, + errors: true, + ...(config.stats as StatsOptions), + } + ); errors.forEach((e) => logger.error(e.message)); warnings.forEach((e) => logger.error(e.message)); @@ -162,5 +221,29 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => }); }; +export const start = async (options: BuilderStartOptions) => { + asyncIterator = starter(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; +}; + +export const build = async (options: BuilderStartOptions) => { + asyncIterator = builder(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; +}; + export const corePresets = [require.resolve('./presets/preview-preset.js')]; export const overridePresets = [require.resolve('./presets/custom-webpack-preset.js')]; diff --git a/lib/cli/src/versions.ts b/lib/cli/src/versions.ts index e8f437411146..594b355ddbd1 100644 --- a/lib/cli/src/versions.ts +++ b/lib/cli/src/versions.ts @@ -1,59 +1,59 @@ // auto generated file, do not edit export default { - "@storybook/addon-a11y": "6.5.0-alpha.57", - "@storybook/addon-actions": "6.5.0-alpha.57", - "@storybook/addon-backgrounds": "6.5.0-alpha.57", - "@storybook/addon-controls": "6.5.0-alpha.57", - "@storybook/addon-docs": "6.5.0-alpha.57", - "@storybook/addon-essentials": "6.5.0-alpha.57", - "@storybook/addon-interactions": "6.5.0-alpha.57", - "@storybook/addon-jest": "6.5.0-alpha.57", - "@storybook/addon-links": "6.5.0-alpha.57", - "@storybook/addon-measure": "6.5.0-alpha.57", - "@storybook/addon-outline": "6.5.0-alpha.57", - "@storybook/addon-storyshots": "6.5.0-alpha.57", - "@storybook/addon-storyshots-puppeteer": "6.5.0-alpha.57", - "@storybook/addon-storysource": "6.5.0-alpha.57", - "@storybook/addon-toolbars": "6.5.0-alpha.57", - "@storybook/addon-viewport": "6.5.0-alpha.57", - "@storybook/addons": "6.5.0-alpha.57", - "@storybook/angular": "6.5.0-alpha.57", - "@storybook/api": "6.5.0-alpha.57", - "@storybook/builder-webpack4": "6.5.0-alpha.57", - "@storybook/builder-webpack5": "6.5.0-alpha.57", - "@storybook/channel-postmessage": "6.5.0-alpha.57", - "@storybook/channel-websocket": "6.5.0-alpha.57", - "@storybook/channels": "6.5.0-alpha.57", - "@storybook/cli": "6.5.0-alpha.57", - "@storybook/client-api": "6.5.0-alpha.57", - "@storybook/client-logger": "6.5.0-alpha.57", - "@storybook/codemod": "6.5.0-alpha.57", - "@storybook/components": "6.5.0-alpha.57", - "@storybook/core": "6.5.0-alpha.57", - "@storybook/core-client": "6.5.0-alpha.57", - "@storybook/core-common": "6.5.0-alpha.57", - "@storybook/core-events": "6.5.0-alpha.57", - "@storybook/core-server": "6.5.0-alpha.57", - "@storybook/csf-tools": "6.5.0-alpha.57", - "@storybook/docs-tools": "6.5.0-alpha.57", - "@storybook/ember": "6.5.0-alpha.57", - "@storybook/html": "6.5.0-alpha.57", - "@storybook/instrumenter": "6.5.0-alpha.57", - "@storybook/manager-webpack4": "6.5.0-alpha.57", - "@storybook/manager-webpack5": "6.5.0-alpha.57", - "@storybook/node-logger": "6.5.0-alpha.57", - "@storybook/postinstall": "6.5.0-alpha.57", - "@storybook/preact": "6.5.0-alpha.57", - "@storybook/preview-web": "6.5.0-alpha.57", - "@storybook/react": "6.5.0-alpha.57", - "@storybook/router": "6.5.0-alpha.57", - "@storybook/server": "6.5.0-alpha.57", - "@storybook/source-loader": "6.5.0-alpha.57", - "@storybook/store": "6.5.0-alpha.57", - "@storybook/svelte": "6.5.0-alpha.57", - "@storybook/theming": "6.5.0-alpha.57", - "@storybook/ui": "6.5.0-alpha.57", - "@storybook/vue": "6.5.0-alpha.57", - "@storybook/vue3": "6.5.0-alpha.57", - "@storybook/web-components": "6.5.0-alpha.57" -} \ No newline at end of file + '@storybook/addon-a11y': '6.5.0-alpha.57', + '@storybook/addon-actions': '6.5.0-alpha.57', + '@storybook/addon-backgrounds': '6.5.0-alpha.57', + '@storybook/addon-controls': '6.5.0-alpha.57', + '@storybook/addon-docs': '6.5.0-alpha.57', + '@storybook/addon-essentials': '6.5.0-alpha.57', + '@storybook/addon-interactions': '6.5.0-alpha.57', + '@storybook/addon-jest': '6.5.0-alpha.57', + '@storybook/addon-links': '6.5.0-alpha.57', + '@storybook/addon-measure': '6.5.0-alpha.57', + '@storybook/addon-outline': '6.5.0-alpha.57', + '@storybook/addon-storyshots': '6.5.0-alpha.57', + '@storybook/addon-storyshots-puppeteer': '6.5.0-alpha.57', + '@storybook/addon-storysource': '6.5.0-alpha.57', + '@storybook/addon-toolbars': '6.5.0-alpha.57', + '@storybook/addon-viewport': '6.5.0-alpha.57', + '@storybook/addons': '6.5.0-alpha.57', + '@storybook/angular': '6.5.0-alpha.57', + '@storybook/api': '6.5.0-alpha.57', + '@storybook/builder-webpack4': '6.5.0-alpha.57', + '@storybook/builder-webpack5': '6.5.0-alpha.57', + '@storybook/channel-postmessage': '6.5.0-alpha.57', + '@storybook/channel-websocket': '6.5.0-alpha.57', + '@storybook/channels': '6.5.0-alpha.57', + '@storybook/cli': '6.5.0-alpha.57', + '@storybook/client-api': '6.5.0-alpha.57', + '@storybook/client-logger': '6.5.0-alpha.57', + '@storybook/codemod': '6.5.0-alpha.57', + '@storybook/components': '6.5.0-alpha.57', + '@storybook/core': '6.5.0-alpha.57', + '@storybook/core-client': '6.5.0-alpha.57', + '@storybook/core-common': '6.5.0-alpha.57', + '@storybook/core-events': '6.5.0-alpha.57', + '@storybook/core-server': '6.5.0-alpha.57', + '@storybook/csf-tools': '6.5.0-alpha.57', + '@storybook/docs-tools': '6.5.0-alpha.57', + '@storybook/ember': '6.5.0-alpha.57', + '@storybook/html': '6.5.0-alpha.57', + '@storybook/instrumenter': '6.5.0-alpha.57', + '@storybook/manager-webpack4': '6.5.0-alpha.57', + '@storybook/manager-webpack5': '6.5.0-alpha.57', + '@storybook/node-logger': '6.5.0-alpha.57', + '@storybook/postinstall': '6.5.0-alpha.57', + '@storybook/preact': '6.5.0-alpha.57', + '@storybook/preview-web': '6.5.0-alpha.57', + '@storybook/react': '6.5.0-alpha.57', + '@storybook/router': '6.5.0-alpha.57', + '@storybook/server': '6.5.0-alpha.57', + '@storybook/source-loader': '6.5.0-alpha.57', + '@storybook/store': '6.5.0-alpha.57', + '@storybook/svelte': '6.5.0-alpha.57', + '@storybook/theming': '6.5.0-alpha.57', + '@storybook/ui': '6.5.0-alpha.57', + '@storybook/vue': '6.5.0-alpha.57', + '@storybook/vue3': '6.5.0-alpha.57', + '@storybook/web-components': '6.5.0-alpha.57', +}; diff --git a/lib/core-server/src/build-static.ts b/lib/core-server/src/build-static.ts index f5dbd8bc1cda..c59532ecd660 100644 --- a/lib/core-server/src/build-static.ts +++ b/lib/core-server/src/build-static.ts @@ -143,7 +143,16 @@ export async function buildStaticStandalone(options: CLIOptions & LoadOptions & options: fullOptions, }); - const [managerStats, previewStats] = await Promise.all([manager, preview]); + const [managerStats, previewStats] = await Promise.all([ + manager.catch(async (err) => { + await previewBuilder.bail(); + throw err; + }), + preview.catch(async (err) => { + await managerBuilder.bail(); + throw err; + }), + ]); if (options.webpackStatsJson) { const target = options.webpackStatsJson === true ? options.outputDir : options.webpackStatsJson; diff --git a/lib/core-server/src/dev-server.ts b/lib/core-server/src/dev-server.ts index 8cc449b53438..4d82c1ff54f6 100644 --- a/lib/core-server/src/dev-server.ts +++ b/lib/core-server/src/dev-server.ts @@ -96,14 +96,20 @@ export async function storybookDevServer(options: Options) { }); const [previewResult, managerResult] = await Promise.all([ - preview, + preview.catch(async (err) => { + await managerBuilder.bail(); + throw err; + }), manager // TODO #13083 Restore this when compiling the preview is fast enough // .then((result) => { // if (!options.ci && !options.smokeTest) openInBrowser(address); // return result; // }) - .catch(previewBuilder.bail), + .catch(async (err) => { + await previewBuilder.bail(); + throw err; + }), ]); // TODO #13083 Remove this when compiling the preview is fast enough diff --git a/lib/manager-webpack4/src/index.ts b/lib/manager-webpack4/src/index.ts index 32d09a906ad0..bc6fb99b470e 100644 --- a/lib/manager-webpack4/src/index.ts +++ b/lib/manager-webpack4/src/index.ts @@ -16,6 +16,19 @@ let compilation: ReturnType; let reject: (reason?: any) => void; type WebpackBuilder = Builder; +type Unpromise> = T extends Promise ? U : never; + +type BuilderStartOptions = Partial['0']>; +type BuilderStartResult = Unpromise>; +type StarterFunction = ( + options: BuilderStartOptions +) => AsyncGenerator; + +type BuilderBuildOptions = Partial['0']>; +type BuilderBuildResult = Unpromise>; +type BuilderFunction = ( + options: BuilderBuildOptions +) => AsyncGenerator; export const WEBPACK_VERSION = '4'; @@ -39,19 +52,64 @@ export const executor = { }, }; -export const start: WebpackBuilder['start'] = async ({ startTime, options, router }) => { +let asyncIterator: ReturnType | ReturnType; + +export const bail: WebpackBuilder['bail'] = async () => { + if (asyncIterator) { + try { + // we tell the builder (that started) to stop ASAP and wait + await asyncIterator.throw(new Error()); + } catch (e) { + // + } + } + if (reject) { + reject(); + } + // we wait for the compiler to finish it's work, so it's command-line output doesn't interfere + return new Promise((res, rej) => { + if (process && compilation) { + try { + compilation.close(() => res()); + logger.warn('Force closed manager build'); + } catch (err) { + logger.warn('Unable to close manager build!'); + res(); + } + } else { + res(); + } + }); +}; + +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. preview builder + * + * I am sorry for making you read about generators today :') + */ +const starter: StarterFunction = async function* starterGeneratorFn({ + startTime, + options, + router, +}) { const prebuiltDir = await getPrebuiltDir(options); if (prebuiltDir && options.managerCache && !options.smokeTest) { logger.info('=> Using prebuilt manager'); router.use('/', express.static(prebuiltDir)); return; } + yield; const config = await getConfig(options); + yield; + if (options.cache) { // Retrieve the Storybook version number to bust cache on upgrades. const packageFile = await findUp('package.json', { cwd: __dirname }); + yield; const { version: storybookVersion } = await fs.readJSON(packageFile); + yield; const cacheKey = `managerConfig-webpack${WEBPACK_VERSION}@${storybookVersion}`; if (options.managerCache) { @@ -61,6 +119,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route useManagerCache(cacheKey, options, config), fs.pathExists(options.outputDir), ]); + yield; if (useCache && hasOutput && !options.smokeTest) { logger.line(1); // force starting new line logger.info('=> Using cached manager'); @@ -68,13 +127,15 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route return; } } else if (!options.smokeTest && (await clearManagerCache(cacheKey, options))) { + yield; logger.line(1); // force starting new line logger.info('=> Cleared cached manager config'); } } const webpackInstance = await executor.get(options); - const compiler = (webpackInstance as any)(config); + yield; + const compiler = webpackInstance(config); if (!compiler) { const err = `${config.name}: missing webpack compiler at runtime!`; @@ -88,6 +149,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route } const { handler, modulesCount } = await useProgressReporting(router, startTime, options); + yield; new ProgressPlugin({ handler, modulesCount }).apply(compiler); const middlewareOptions: Parameters[1] = { @@ -104,9 +166,10 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route compilation.waitUntilValid(ready); reject = stop; }); + yield; if (!stats) { - throw new Error('no stats after building preview'); + throw new Error('no stats after building manager'); } // eslint-disable-next-line consistent-return @@ -117,26 +180,31 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route }; }; -export const bail: WebpackBuilder['bail'] = (e: Error) => { - if (reject) { - reject(); - } - if (process) { - try { - compilation.close(); - logger.warn('Force closed preview build'); - } catch (err) { - logger.warn('Unable to close preview build!'); - } - } - throw e; +export const start = async (options: BuilderStartOptions) => { + asyncIterator = starter(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; }; -export const build: WebpackBuilder['build'] = async ({ options, startTime }) => { +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. preview builder + * + * I am sorry for making you read about generators today :') + */ +const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, options }) { logger.info('=> Compiling manager..'); const webpackInstance = await executor.get(options); - + yield; const config = await getConfig(options); + yield; + const statsOptions = typeof config.stats === 'boolean' ? 'minimal' : config.stats; const compiler = webpackInstance(config); @@ -145,8 +213,9 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => logger.error(err); return Promise.resolve(makeStatsFromError(err)); } + yield; - return new Promise((succeed, fail) => { + return new Promise((succeed, fail) => { compiler.run((error, stats) => { if (error || !stats || stats.hasErrors()) { logger.error('=> Failed to build the manager'); @@ -177,6 +246,18 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => }); }; +export const build = async (options: BuilderStartOptions) => { + asyncIterator = builder(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; +}; + export const corePresets: WebpackBuilder['corePresets'] = [ require.resolve('./presets/manager-preset'), ]; diff --git a/lib/manager-webpack5/src/index.ts b/lib/manager-webpack5/src/index.ts index 11e2fbeefe93..dfb134545f3b 100644 --- a/lib/manager-webpack5/src/index.ts +++ b/lib/manager-webpack5/src/index.ts @@ -16,6 +16,19 @@ let compilation: ReturnType; let reject: (reason?: any) => void; type WebpackBuilder = Builder; +type Unpromise> = T extends Promise ? U : never; + +type BuilderStartOptions = Partial['0']>; +type BuilderStartResult = Unpromise>; +type StarterFunction = ( + options: BuilderStartOptions +) => AsyncGenerator; + +type BuilderBuildOptions = Partial['0']>; +type BuilderBuildResult = Unpromise>; +type BuilderFunction = ( + options: BuilderBuildOptions +) => AsyncGenerator; export const WEBPACK_VERSION = '5'; @@ -39,15 +52,58 @@ export const executor = { }, }; -export const start: WebpackBuilder['start'] = async ({ startTime, options, router }) => { +let asyncIterator: ReturnType | ReturnType; + +export const bail: WebpackBuilder['bail'] = async () => { + if (asyncIterator) { + try { + // we tell the builder (that started) to stop ASAP and wait + await asyncIterator.throw(new Error()); + } catch (e) { + // + } + } + if (reject) { + reject(); + } + // we wait for the compiler to finish it's work, so it's command-line output doesn't interfere + return new Promise((res, rej) => { + if (process && compilation) { + try { + compilation.close(() => res()); + logger.warn('Force closed manager build'); + } catch (err) { + logger.warn('Unable to close manager build!'); + res(); + } + } else { + res(); + } + }); +}; + +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. preview builder + * + * I am sorry for making you read about generators today :') + */ +const starter: StarterFunction = async function* starterGeneratorFn({ + startTime, + options, + router, +}) { const prebuiltDir = await getPrebuiltDir(options); if (prebuiltDir && options.managerCache && !options.smokeTest) { logger.info('=> Using prebuilt manager'); router.use('/', express.static(prebuiltDir)); return; } + yield; const config = await getConfig(options); + yield; + if (options.cache) { // Retrieve the Storybook version number to bust cache on upgrades. const packageFile = await findUp('package.json', { cwd: __dirname }); @@ -61,6 +117,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route useManagerCache(cacheKey, options, config), fs.pathExists(options.outputDir), ]); + yield; if (useCache && hasOutput && !options.smokeTest) { logger.line(1); // force starting new line logger.info('=> Using cached manager'); @@ -68,12 +125,14 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route return; } } else if (!options.smokeTest && (await clearManagerCache(cacheKey, options))) { + yield; logger.line(1); // force starting new line logger.info('=> Cleared cached manager config'); } } const webpackInstance = await executor.get(options); + yield; const compiler = (webpackInstance as any)(config); if (!compiler) { @@ -88,6 +147,7 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route } const { handler, modulesCount } = await useProgressReporting(router, startTime, options); + yield; new ProgressPlugin({ handler, modulesCount }).apply(compiler); const middlewareOptions: Parameters[1] = { @@ -103,9 +163,10 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route compilation.waitUntilValid(ready); reject = stop; }); + yield; if (!stats) { - throw new Error('no stats after building preview'); + throw new Error('no stats after building manager'); } // eslint-disable-next-line consistent-return @@ -116,26 +177,30 @@ export const start: WebpackBuilder['start'] = async ({ startTime, options, route }; }; -export const bail: WebpackBuilder['bail'] = (e: Error) => { - if (reject) { - reject(); - } - if (process) { - try { - compilation.close(); - logger.warn('Force closed preview build'); - } catch (err) { - logger.warn('Unable to close preview build!'); - } - } - throw e; +export const start = async (options: BuilderStartOptions) => { + asyncIterator = starter(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; }; -export const build: WebpackBuilder['build'] = async ({ options, startTime }) => { +/** + * This function is a generator so that we can abort it mid process + * in case of failure coming from other processes e.g. preview builder + * + * I am sorry for making you read about generators today :') + */ +const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, options }) { logger.info('=> Compiling manager..'); const webpackInstance = await executor.get(options); - + yield; const config = await getConfig(options); + yield; const compiler = webpackInstance(config); if (!compiler) { @@ -143,8 +208,9 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => logger.error(err); return Promise.resolve(makeStatsFromError(err)); } + yield; - return new Promise((succeed, fail) => { + return new Promise((succeed, fail) => { compiler.run((error, stats) => { if (error || !stats || stats.hasErrors()) { logger.error('=> Failed to build the manager'); @@ -174,6 +240,18 @@ export const build: WebpackBuilder['build'] = async ({ options, startTime }) => }); }; +export const build = async (options: BuilderStartOptions) => { + asyncIterator = builder(options); + let result; + + do { + // eslint-disable-next-line no-await-in-loop + result = await asyncIterator.next(); + } while (!result.done); + + return result.value; +}; + export const corePresets: WebpackBuilder['corePresets'] = [ require.resolve('./presets/manager-preset'), ];