Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 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,16 @@
/* eslint-disable local-rules/no-uncategorized-errors */
import { WebpackCompilationError } from './server-errors';

describe('WebpackCompilationError', () => {
it('should correctly handle error with stats.compilation.errors', () => {
const errors = [
new Error('Error 1 \u001B[4mmessage\u001B[0m'),
new Error('\u001B[4mError\u001B[0m 2 message'),
];

const webpackError = new WebpackCompilationError({ errors });

expect(webpackError.data.errors[0].message).toEqual('Error 1 message');
expect(webpackError.data.errors[1].message).toEqual('Error 2 message');
});
});
79 changes: 79 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,82 @@ 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((err) => {
return {
...err,
message: removeAnsiEscapeCodes(err.message),
stack: removeAnsiEscapeCodes(err.stack),
name: err.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
5 changes: 5 additions & 0 deletions code/lib/core-server/src/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { CoreConfig, Options, StorybookConfig } from '@storybook/types';

import { logConfig } from '@storybook/core-common';

import { logger } from '@storybook/node-logger';
import { getMiddleware } from './utils/middleware';
import { getServerAddresses } from './utils/server-address';
import { getServer } from './utils/server-init';
Expand Down Expand Up @@ -90,6 +91,7 @@ export async function storybookDevServer(options: Options) {
let previewStarted: Promise<any> = Promise.resolve();

if (!options.ignorePreview) {
logger.info('=> Starting preview..');
previewStarted = previewBuilder
.start({
startTime: process.hrtime(),
Expand All @@ -99,6 +101,9 @@ export async function storybookDevServer(options: Options) {
channel: serverChannel,
})
.catch(async (e: any) => {
logger.error('=> Failed to build the preview');
process.exitCode = 1;

await managerBuilder?.bail().catch();
// For some reason, even when Webpack fails e.g. wrong main.js config,
// the preview may continue to print to stdout, which can affect output
Expand Down