mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 10:17:55 +02:00
fix(core): better error logging on SSR/dev failures + log stacktraces and error causes (#8872)
This commit is contained in:
parent
46d2aa231d
commit
a9a5f89b9f
13 changed files with 155 additions and 75 deletions
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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) => {
|
||||
|
|
46
website/_dogfooding/_pages tests/crashTest.tsx
Normal file
46
website/_dogfooding/_pages tests/crashTest.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.',
|
||||
|
|
Loading…
Add table
Reference in a new issue