Skip to content

Commit

Permalink
Merge pull request #17809 from storybookjs/feat/cancelable-builder-pr…
Browse files Browse the repository at this point in the history
…ocess

Webpack: Make manager and preview build processes cancelable
  • Loading branch information
ndelangen authored Apr 6, 2022
2 parents 645f5ab + 1da2437 commit 32419be
Show file tree
Hide file tree
Showing 7 changed files with 492 additions and 149 deletions.
130 changes: 108 additions & 22 deletions lib/builder-webpack4/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ let compilation: ReturnType<typeof webpackDevMiddleware>;
let reject: (reason?: any) => void;

type WebpackBuilder = Builder<Configuration, Stats>;
type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never;

type BuilderStartOptions = Partial<Parameters<WebpackBuilder['start']>['0']>;
type BuilderStartResult = Unpromise<ReturnType<WebpackBuilder['start']>>;
type StarterFunction = (
options: BuilderStartOptions
) => AsyncGenerator<unknown, BuilderStartResult, void>;

type BuilderBuildOptions = Partial<Parameters<WebpackBuilder['build']>['0']>;
type BuilderBuildResult = Unpromise<ReturnType<WebpackBuilder['build']>>;
type BuilderFunction = (
options: BuilderBuildOptions
) => AsyncGenerator<unknown, BuilderBuildResult, void>;

export const executor = {
get: async (options: Options) => {
Expand Down Expand Up @@ -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<BuilderFunction> | ReturnType<StarterFunction>;

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);
Expand All @@ -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<typeof webpackDevMiddleware>[1] = {
Expand All @@ -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');
Expand All @@ -102,35 +160,28 @@ 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) {
const err = `${config.name}: missing webpack compiler at runtime!`;
logger.error(err);
return Promise.resolve(makeStatsFromError(err));
}
yield;

return new Promise((succeed, fail) => {
return new Promise<Stats>((succeed, fail) => {
compiler.run((error, stats) => {
if (error || !stats || stats.hasErrors()) {
logger.error('=> Failed to build the preview');
Expand All @@ -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'));
}
}

Expand All @@ -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')];
Loading

0 comments on commit 32419be

Please sign in to comment.