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

Builder-Webpack5: Categorize builder error #24031

Merged
merged 5 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
131 changes: 65 additions & 66 deletions code/builders/builder-webpack5/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { dirname, join, parse } from 'path';
import express from 'express';
import fs from 'fs-extra';
import { PREVIEW_BUILDER_PROGRESS } from '@storybook/core-events';
import {
WebpackCompilationError,
WebpackInvocationError,
WebpackMissingStatsError,
} from '@storybook/core-events/server-errors';

import prettyTime from 'pretty-hrtime';

Expand Down Expand Up @@ -117,21 +122,19 @@ const starter: StarterFunction = async function* starterGeneratorFn({
yield;

const config = await getConfig(options);

if (config.stats === 'none' || config.stats === 'summary') {
throw new WebpackMissingStatsError();
}
yield;

const compiler = webpackInstance(config);

if (!compiler) {
const err = `${config.name}: missing webpack compiler at runtime!`;
logger.error(err);
return {
bail,
totalTime: process.hrtime(startTime),
stats: {
hasErrors: () => true,
hasWarnings: () => false,
toJson: () => ({ warnings: [] as any[], errors: [err] }),
} as any as Stats,
};
throw new WebpackInvocationError({
// eslint-disable-next-line local-rules/no-uncategorized-errors
error: new Error(`Missing Webpack compiler at runtime!`),
});
}

yield;
Expand Down Expand Up @@ -172,6 +175,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({
const middlewareOptions: Parameters<typeof webpackDevMiddleware>[1] = {
publicPath: config.output?.publicPath as string,
writeToDisk: true,
stats: 'errors-only',
};

compilation = webpackDevMiddleware(compiler, middlewareOptions);
Expand All @@ -184,19 +188,24 @@ const starter: StarterFunction = async function* starterGeneratorFn({
router.use(compilation);
router.use(webpackHotMiddleware(compiler, { log: false }));

const stats = await new Promise<Stats>((ready, stop) => {
compilation?.waitUntilValid(ready as any);
reject = stop;
const stats = await new Promise<Stats>((res, rej) => {
compilation?.waitUntilValid(res as any);
reject = rej;
});
yield;

if (!stats) {
throw new Error('no stats after building preview');
throw new WebpackMissingStatsError();
}

if (stats.hasErrors()) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw stats;
const { warnings, errors } = getWebpackStats({ config, stats });

if (warnings.length > 0) {
warnings?.forEach((e) => logger.error(e.message));
}

if (errors.length > 0) {
throw new WebpackCompilationError({ errors });
}

return {
Expand All @@ -206,6 +215,22 @@ const starter: StarterFunction = async function* starterGeneratorFn({
};
};

function getWebpackStats({ config, stats }: { config: Configuration; stats: Stats }) {
const statsOptions =
typeof config.stats === 'string'
? config.stats
: {
...(config.stats as StatsOptions),
warnings: true,
errors: true,
};
const { warnings = [], errors = [] } = stats?.toJson(statsOptions) || {};
return {
warnings,
errors,
};
}

/**
* 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
Expand All @@ -215,73 +240,47 @@ const starter: StarterFunction = async function* starterGeneratorFn({
const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, options }) {
const webpackInstance = await executor.get(options);
yield;
logger.info('=> Compiling preview..');
const config = await getConfig(options);

if (config.stats === 'none' || config.stats === 'summary') {
throw new WebpackMissingStatsError();
}
yield;

const compiler = webpackInstance(config);

if (!compiler) {
const err = `${config.name}: missing webpack compiler at runtime!`;
logger.error(err);
return {
hasErrors: () => true,
hasWarnings: () => false,
toJson: () => ({ warnings: [] as any[], errors: [err] }),
} as any as Stats;
throw new WebpackInvocationError({
// eslint-disable-next-line local-rules/no-uncategorized-errors
error: new Error(`Missing Webpack compiler at runtime!`),
});
}

const webpackCompilation = new Promise<Stats>((succeed, fail) => {
compiler.run((error, stats) => {
if (error || !stats || stats.hasErrors()) {
logger.error('=> Failed to build the preview');
process.exitCode = 1;

if (error) {
logger.error(error.message);
if (error) {
compiler.close(() => fail(new WebpackInvocationError({ error })));
return;
}

compiler.close(() => fail(error));
if (!stats) {
throw new WebpackMissingStatsError();
}

return;
}
const { warnings, errors } = getWebpackStats({ config, stats });

if (stats && (stats.hasErrors() || stats.hasWarnings())) {
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));

compiler.close(() =>
options.debugWebpack
? fail(stats)
: fail(new Error('=> Webpack failed, learn more with --debug-webpack'))
);

return;
}
if (warnings.length > 0) {
warnings?.forEach((e) => logger.error(e.message));
}

logger.trace({ message: '=> Preview built', time: process.hrtime(startTime) });
if (stats && stats.hasWarnings()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know it has warnings because of hasWarnings()
stats
.toJson({ warnings: true } as StatsOptions)
.warnings!.forEach((e) => logger.warn(e.message));
if (errors.length > 0) {
compiler.close(() => fail(new WebpackCompilationError({ errors })));
return;
}

// https://webpack.js.org/api/node/#run
// #15227
compiler.close((closeErr) => {
if (closeErr) {
return fail(closeErr);
return fail(new WebpackInvocationError({ error: closeErr }));
}

return succeed(stats as Stats);
Expand Down
80 changes: 80 additions & 0 deletions code/lib/core-events/src/errors/server-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable local-rules/no-uncategorized-errors */
import { WebpackCompilationError } from './server-errors';

describe('WebpackCompilationError', () => {
it('should correctly handle error with error property', () => {
const error = new Error('Custom error message');
const data = {
error,
};

const webpackError = new WebpackCompilationError(data);

expect(webpackError.message).toBe(error.message);
});

it('should correctly handle error with error within error', () => {
const error = new Error('Custom error message');
const data = {
error: new Error() as Error & { error: Error },
};
data.error.error = error;

const webpackError = new WebpackCompilationError(data);

expect(webpackError.message).toBe(error.message);
});

it('should correctly handle error with stats.compilation.errors', () => {
const compilationErrors = [new Error('Error 1 message'), new Error('Error 2 message')];
const data = new Error() as Error & {
error: Error & { stats?: { compilation: { errors: Error[] } } };
};
data.error = new Error();
data.error.stats = {
compilation: {
errors: compilationErrors,
},
};

const webpackError = new WebpackCompilationError(data);

expect(webpackError.message).toMatchInlineSnapshot(`
"Error: Error 1 message

Error: Error 2 message"
`);
});

it('should correctly handle object with compilation.errors', () => {
const compilationErrors = [new Error('Error 1 message'), new Error('Error 2 message')];
const data = {
error: {
compilation: {
errors: compilationErrors,
},
},
};

const webpackError = new WebpackCompilationError(data);

expect(webpackError.message).toMatchInlineSnapshot(`
"Error: Error 1 message

Error: Error 2 message"
`);
});

it('should correctly handle error without specific format', () => {
const errorMessage = 'Generic error message';
const data = new Error() as Error & {
error: Error;
};

data.error = new Error(errorMessage);

const webpackError = new WebpackCompilationError(data);

expect(webpackError.message).toBe(errorMessage);
});
});
78 changes: 78 additions & 0 deletions code/lib/core-events/src/errors/server-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,81 @@ export class InvalidStoriesEntryError extends StorybookError {
`;
}
}

export class WebpackMissingStatsError extends StorybookError {
readonly category = Category.BUILDER_WEBPACK5;

readonly code = 1;

public documentation = [
'https://webpack.js.org/configuration/stats/',
'https://storybook.js.org/docs/react/builders/webpack#configure',
];

template() {
return dedent`
No Webpack stats found. Did you turn off stats reporting in your webpack config?
Storybook needs Webpack stats (including errors) in order to build correctly.
`;
}
}

export class WebpackInvocationError extends StorybookError {
readonly category = Category.BUILDER_WEBPACK5;

readonly code = 2;

private errorMessage = '';

constructor(
public data: {
error: Error;
}
) {
super();
this.errorMessage = data.error.message;
}

template() {
return this.errorMessage.trim();
}
}
function removeAnsiEscapeCodes(input = '') {
// eslint-disable-next-line no-control-regex
return input.replace(/\u001B\[[0-9;]*m/g, '');
}

export class WebpackCompilationError extends StorybookError {
readonly category = Category.BUILDER_WEBPACK5;

readonly code = 3;

constructor(
public data: {
errors: {
message: string;
stack?: string;
name?: string;
}[];
}
) {
super();

this.data.errors = data.errors.map((e, i) => {
return {
...e,
message: removeAnsiEscapeCodes(e.message),
stack: removeAnsiEscapeCodes(e.stack),
name: e.name,
};
});
}

template() {
// This error message is a followup of errors logged by Webpack to the user
return dedent`
There were problems when compiling your code with Webpack.
Run Storybook with --debug-webpack for more information.
`;
}
}
12 changes: 11 additions & 1 deletion code/lib/core-server/src/build-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,23 +199,33 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption

if (options.ignorePreview) {
logger.info(`=> Not building preview`);
} else {
logger.info('=> Building preview..');
}

const startTime = process.hrtime();
await Promise.all([
...(options.ignorePreview
? []
: [
previewBuilder
.build({
startTime: process.hrtime(),
startTime,
options: fullOptions,
})
.then(async (previewStats) => {
logger.trace({ message: '=> Preview built', time: process.hrtime(startTime) });

if (options.webpackStatsJson) {
const target =
options.webpackStatsJson === true ? options.outputDir : options.webpackStatsJson;
await outputStats(target, previewStats);
}
})
.catch((error) => {
logger.error('=> Failed to build the preview');
process.exitCode = 1;
throw error;
}),
]),
...effects,
Expand Down
Loading