Skip to content

Commit

Permalink
fix(core): better error logging on SSR/dev failures + log stacktraces…
Browse files Browse the repository at this point in the history
… and error causes (#8872)
  • Loading branch information
slorber committed Apr 7, 2023
1 parent 46d2aa2 commit a9a5f89
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 75 deletions.
1 change: 1 addition & 0 deletions packages/docusaurus-plugin-pwa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions packages/docusaurus-plugin-pwa/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
}),
Expand Down
6 changes: 5 additions & 1 deletion packages/docusaurus/bin/docusaurus.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
54 changes: 34 additions & 20 deletions packages/docusaurus/src/client/serverEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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;
}
}
8 changes: 6 additions & 2 deletions packages/docusaurus/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
17 changes: 13 additions & 4 deletions packages/docusaurus/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
applyConfigureWebpack,
applyConfigurePostCss,
getHttpsConfig,
formatStatsErrorMessage,
printStatsWarnings,
} from '../webpack/utils';
import {getHostPort, type HostPortOptions} from '../server/getHostPort';

Expand Down Expand Up @@ -170,16 +172,23 @@ export async function start(
});

const compiler = webpack(config);
if (process.env.E2E_TEST) {
compiler.hooks.done.tap('done', (stats) => {
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 = {
Expand Down
10 changes: 7 additions & 3 deletions packages/docusaurus/src/webpack/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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',
}),
],
Expand All @@ -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);
}
Expand Down
35 changes: 0 additions & 35 deletions packages/docusaurus/src/webpack/plugins/LogPlugin.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/docusaurus/src/webpack/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -99,7 +99,7 @@ export default async function createServerConfig({
}),

// Show compilation progress bar.
new LogPlugin({
new WebpackBar({
name: 'Server',
color: 'yellow',
}),
Expand Down
38 changes: 32 additions & 6 deletions packages/docusaurus/src/webpack/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down
46 changes: 46 additions & 0 deletions website/_dogfooding/_pages tests/crashTest.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
1 change: 1 addition & 0 deletions website/_dogfooding/_pages tests/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions website/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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.',
Expand Down

0 comments on commit a9a5f89

Please sign in to comment.