fix(core): better error logging on SSR/dev failures + log stacktraces and error causes (#8872)

This commit is contained in:
Sébastien Lorber 2023-04-07 18:00:59 +01:00 committed by GitHub
parent 46d2aa231d
commit a9a5f89b9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 155 additions and 75 deletions

View file

@ -35,6 +35,7 @@
"tslib": "^2.5.0",
"webpack": "^5.76.0",
"webpack-merge": "^5.8.0",
"webpackbar": "^5.0.2",
"workbox-build": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-window": "^6.5.4"

View file

@ -7,11 +7,11 @@
import path from 'path';
import webpack, {type Configuration} from 'webpack';
import WebpackBar from 'webpackbar';
import Terser from 'terser-webpack-plugin';
import {injectManifest} from 'workbox-build';
import {normalizeUrl} from '@docusaurus/utils';
import {compile} from '@docusaurus/core/lib/webpack/utils';
import LogPlugin from '@docusaurus/core/lib/webpack/plugins/LogPlugin';
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
import type {PluginOptions} from '@docusaurus/plugin-pwa';
@ -160,7 +160,7 @@ export default function pluginPWA(
// Fallback value required with Webpack 5
PWA_SW_CUSTOM: swCustom ?? '',
}),
new LogPlugin({
new WebpackBar({
name: 'Service Worker',
color: 'red',
}),

View file

@ -244,7 +244,11 @@ if (!process.argv.slice(2).length) {
cli.parse(process.argv);
process.on('unhandledRejection', (err) => {
logger.error(err instanceof Error ? err.stack : err);
console.log('');
// Do not use logger.error here: it does not print error causes
console.error(err);
console.log('');
logger.info`Docusaurus version: number=${DOCUSAURUS_VERSION}
Node version: number=${process.version}`;
process.exit(1);

View file

@ -10,7 +10,6 @@ import path from 'path';
import fs from 'fs-extra';
// eslint-disable-next-line no-restricted-imports
import _ from 'lodash';
import chalk from 'chalk';
import * as eta from 'eta';
import {StaticRouter} from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
@ -37,29 +36,43 @@ function renderSSRTemplate(ssrTemplate: string, data: object) {
return compiled(data, eta.defaultConfig);
}
function buildSSRErrorMessage({
error,
pathname,
}: {
error: Error;
pathname: string;
}): string {
const parts = [
`Docusaurus server-side rendering could not render static page with path ${pathname} because of error: ${error.message}`,
];
const isNotDefinedErrorRegex =
/(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;
if (isNotDefinedErrorRegex.test(error.message)) {
// prettier-ignore
parts.push(`It looks like you are using code that should run on the client-side only.
To get around it, try using \`<BrowserOnly>\` (https://docusaurus.io/docs/docusaurus-core/#browseronly) or \`ExecutionEnvironment\` (https://docusaurus.io/docs/docusaurus-core/#executionenvironment).
It might also require to wrap your client code in \`useEffect\` hook and/or import a third-party library dynamically (if any).`);
}
return parts.join('\n');
}
export default async function render(
locals: Locals & {path: string},
): Promise<string> {
try {
return await doRender(locals);
} catch (err) {
// We are not using logger in this file, because it seems to fail with some
// compilers / some polyfill methods. This is very likely a bug, but in the
// long term, when we output native ES modules in SSR, the bug will be gone.
// prettier-ignore
console.error(chalk.red(`${chalk.bold('[ERROR]')} Docusaurus server-side rendering could not render static page with path ${chalk.cyan.underline(locals.path)}.`));
const isNotDefinedErrorRegex =
/(?:window|document|localStorage|navigator|alert|location|buffer|self) is not defined/i;
if (isNotDefinedErrorRegex.test((err as Error).message)) {
// prettier-ignore
console.info(`${chalk.cyan.bold('[INFO]')} It looks like you are using code that should run on the client-side only.
To get around it, try using ${chalk.cyan('`<BrowserOnly>`')} (${chalk.cyan.underline('https://docusaurus.io/docs/docusaurus-core/#browseronly')}) or ${chalk.cyan('`ExecutionEnvironment`')} (${chalk.cyan.underline('https://docusaurus.io/docs/docusaurus-core/#executionenvironment')}).
It might also require to wrap your client code in ${chalk.cyan('`useEffect`')} hook and/or import a third-party library dynamically (if any).`);
}
throw err;
} catch (errorUnknown) {
const error = errorUnknown as Error;
const message = buildSSRErrorMessage({error, pathname: locals.path});
const ssrError = new Error(message, {cause: error});
// It is important to log the error here because the stacktrace causal chain
// is not available anymore upper in the tree (this SSR runs in eval)
console.error(ssrError);
throw ssrError;
}
}
@ -158,7 +171,8 @@ async function doRender(locals: Locals & {path: string}) {
});
} catch (err) {
// prettier-ignore
console.error(chalk.red(`${chalk.bold('[ERROR]')} Minification of page ${chalk.cyan.underline(locals.path)} failed.`));
console.error(`Minification of page ${locals.path} failed.`);
console.error(err);
throw err;
}
}

View file

@ -72,8 +72,12 @@ export async function build(
isLastLocale,
});
} catch (err) {
logger.error`Unable to build website for locale name=${locale}.`;
throw err;
throw new Error(
logger.interpolate`Unable to build website for locale name=${locale}.`,
{
cause: err,
},
);
}
}
const context = await loadContext({

View file

@ -24,6 +24,8 @@ import {
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
formatStatsErrorMessage,
printStatsWarnings,
} from '../webpack/utils';
import {getHostPort, type HostPortOptions} from '../server/getHostPort';
@ -170,16 +172,23 @@ export async function start(
});
const compiler = webpack(config);
if (process.env.E2E_TEST) {
compiler.hooks.done.tap('done', (stats) => {
const errorsWarnings = stats.toJson('errors-warnings');
const statsErrorMessage = formatStatsErrorMessage(errorsWarnings);
if (statsErrorMessage) {
console.error(statsErrorMessage);
}
printStatsWarnings(errorsWarnings);
if (process.env.E2E_TEST) {
if (stats.hasErrors()) {
logger.error('E2E_TEST: Project has compiler errors.');
process.exit(1);
}
logger.success('E2E_TEST: Project can compile.');
process.exit(0);
});
}
});
// https://webpack.js.org/configuration/dev-server
const defaultDevServerConfig: WebpackDevServer.Configuration = {

View file

@ -8,9 +8,10 @@
import path from 'path';
import logger from '@docusaurus/logger';
import merge from 'webpack-merge';
import WebpackBar from 'webpackbar';
import {createBaseConfig} from './base';
import ChunkAssetPlugin from './plugins/ChunkAssetPlugin';
import LogPlugin from './plugins/LogPlugin';
import {formatStatsErrorMessage} from './utils';
import type {Props} from '@docusaurus/types';
import type {Configuration} from 'webpack';
@ -34,7 +35,7 @@ export default async function createClientConfig(
plugins: [
new ChunkAssetPlugin(),
// Show compilation progress bar and build time.
new LogPlugin({
new WebpackBar({
name: 'Client',
}),
],
@ -47,8 +48,11 @@ export default async function createClientConfig(
apply: (compiler) => {
compiler.hooks.done.tap('client:done', (stats) => {
if (stats.hasErrors()) {
const errorsWarnings = stats.toJson('errors-warnings');
logger.error(
'Client bundle compiled with errors therefore further build is impossible.',
`Client bundle compiled with errors therefore further build is impossible.\n${formatStatsErrorMessage(
errorsWarnings,
)}`,
);
process.exit(1);
}

View file

@ -1,35 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import WebpackBar from 'webpackbar';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import type {Compiler} from 'webpack';
function showError(arr: string[]) {
console.log(`\n\n${arr.join('\n')}`);
}
export default class LogPlugin extends WebpackBar {
override apply(compiler: Compiler): void {
super.apply(compiler);
// TODO can't this be done in compile(configs) alongside the warnings???
compiler.hooks.done.tap('DocusaurusLogPlugin', (stats) => {
if (stats.hasErrors()) {
const errorsWarnings = stats.toJson('errors-warnings');
// TODO do we really want to keep this legacy logic?
// let's wait and see how the react-dev-utils support Webpack5
// we probably want to print the error stacktraces here
const messages = formatWebpackMessages(errorsWarnings);
if (messages.errors.length) {
showError(messages.errors);
}
}
});
}
}

View file

@ -16,9 +16,9 @@ import {
import StaticSiteGeneratorPlugin, {
type Locals,
} from '@slorber/static-site-generator-webpack-plugin';
import WebpackBar from 'webpackbar';
import {createBaseConfig} from './base';
import WaitPlugin from './plugins/WaitPlugin';
import LogPlugin from './plugins/LogPlugin';
import ssrDefaultTemplate from './templates/ssr.html.template';
import type {Props} from '@docusaurus/types';
import type {Configuration} from 'webpack';
@ -99,7 +99,7 @@ export default async function createServerConfig({
}),
// Show compilation progress bar.
new LogPlugin({
new WebpackBar({
name: 'Server',
color: 'yellow',
}),

View file

@ -23,6 +23,7 @@ import webpack, {
} from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
import type {CustomOptions, CssNanoOptions} from 'css-minimizer-webpack-plugin';
import type {TransformOptions} from '@babel/core';
import type {
@ -31,6 +32,29 @@ import type {
ConfigureWebpackUtils,
} from '@docusaurus/types';
export function formatStatsErrorMessage(
statsJson: ReturnType<webpack.Stats['toJson']> | undefined,
): string | undefined {
if (statsJson?.errors?.length) {
// TODO formatWebpackMessages does not print stack-traces
// Also the error causal chain is lost here
// We log the stacktrace inside serverEntry.tsx for now (not ideal)
const {errors} = formatWebpackMessages(statsJson);
return errors.join('\n---\n');
}
return undefined;
}
export function printStatsWarnings(
statsJson: ReturnType<webpack.Stats['toJson']> | undefined,
): void {
if (statsJson?.warnings?.length) {
statsJson.warnings?.forEach((warning) => {
logger.warn(warning);
});
}
}
// Utility method to get style loaders
export function getStyleLoaders(
isServer: boolean,
@ -250,13 +274,15 @@ export function compile(config: Configuration[]): Promise<void> {
// Let plugins consume all the stats
const errorsWarnings = stats?.toJson('errors-warnings');
if (stats?.hasErrors()) {
reject(new Error('Failed to compile with errors.'));
}
if (errorsWarnings && stats?.hasWarnings()) {
errorsWarnings.warnings?.forEach((warning) => {
logger.warn(warning);
});
const statsErrorMessage = formatStatsErrorMessage(errorsWarnings);
reject(
new Error(
`Failed to compile due to Webpack errors.\n${statsErrorMessage}`,
),
);
}
printStatsWarnings(errorsWarnings);
// Webpack 5 requires calling close() so that persistent caching works
// See https://github.com/webpack/webpack.js.org/pull/4775
compiler.close((errClose) => {

View file

@ -0,0 +1,46 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
// We only crash the page if siteConfig.customFields.crashTest === true
function useBoom(): boolean {
const {
siteConfig: {customFields},
} = useDocusaurusContext();
return (customFields as {crashTest?: boolean}).crashTest ?? false;
}
function boomRoot() {
throw new Error('Boom root');
}
function boomParent() {
try {
boomRoot();
} catch (err) {
throw new Error('Boom parent', {cause: err as Error});
}
}
function BoomComponent() {
const boom = useBoom();
return <>{boom && boomParent()}</>;
}
export default function CrashTestPage(): JSX.Element {
return (
<Layout>
{/* eslint-disable-next-line @docusaurus/prefer-docusaurus-heading */}
<h1>This crash if customFields.crashTest = true</h1>
<BoomComponent />
</Layout>
);
}

View file

@ -20,6 +20,7 @@ import Readme from "../README.mdx"
### Other tests
- [Crash test](/tests/pages/crashTest)
- [Code block tests](/tests/pages/code-block-tests)
- [Link tests](/tests/pages/link-tests)
- [Error boundary tests](/tests/pages/error-boundary-tests)

View file

@ -42,6 +42,11 @@ function getNextVersionName() {
*/
}
// Artificial way to crash the SSR rendering and test errors
// See website/_dogfooding/_pages tests/crashTest.tsx
// Test with: DOCUSAURUS_CRASH_TEST=true yarn build:website:fast
const crashTest = process.env.DOCUSAURUS_CRASH_TEST === 'true';
const isDev = process.env.NODE_ENV === 'development';
const isDeployPreview =
@ -139,6 +144,7 @@ const config = {
onBrokenMarkdownLinks: 'warn',
favicon: 'img/docusaurus.ico',
customFields: {
crashTest,
isDeployPreview,
description:
'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.',