Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webpack: Make manager and preview build processes cancelable #17809

Merged
merged 13 commits into from
Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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