From aa66c4ed5e084d98dc03a2a375f271efa97829b3 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 7 Apr 2023 17:58:29 +0200 Subject: [PATCH 1/3] Fix error logging on SSR/dev failures --- packages/docusaurus/bin/docusaurus.mjs | 5 ++ .../docusaurus/src/client/serverEntry.tsx | 53 ++++++++++++------- packages/docusaurus/src/commands/build.ts | 1 + packages/docusaurus/src/commands/start.ts | 17 ++++-- packages/docusaurus/src/webpack/client.ts | 10 ++-- .../src/webpack/plugins/LogPlugin.ts | 35 ------------ packages/docusaurus/src/webpack/server.ts | 4 +- packages/docusaurus/src/webpack/utils.ts | 38 ++++++++++--- .../_dogfooding/_pages tests/crashTest.tsx | 46 ++++++++++++++++ website/_dogfooding/_pages tests/index.mdx | 1 + website/docusaurus.config.js | 6 +++ 11 files changed, 146 insertions(+), 70 deletions(-) delete mode 100644 packages/docusaurus/src/webpack/plugins/LogPlugin.ts create mode 100644 website/_dogfooding/_pages tests/crashTest.tsx diff --git a/packages/docusaurus/bin/docusaurus.mjs b/packages/docusaurus/bin/docusaurus.mjs index 00670612b9f8..c631379197d2 100755 --- a/packages/docusaurus/bin/docusaurus.mjs +++ b/packages/docusaurus/bin/docusaurus.mjs @@ -244,7 +244,12 @@ if (!process.argv.slice(2).length) { cli.parse(process.argv); process.on('unhandledRejection', (err) => { + // TODO this logger thing is annoying + // This makes it impossible to use native logging of nested stacktraces + // ie the error causes are not logged here + // It seems we can't have colors + nested stacktrace logging at the same time logger.error(err instanceof Error ? err.stack : err); + logger.info`Docusaurus version: number=${DOCUSAURUS_VERSION} Node version: number=${process.version}`; process.exit(1); diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index f353c05bfdbd..628ec5da7cd8 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -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 = [ + `[ERROR] 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 \`\` (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 { 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('``')} (${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,7 @@ 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(`[ERROR] Minification of page ${locals.path} failed.`); throw err; } } diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 9d33f8bc8392..4fa4a4fdfe75 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -72,6 +72,7 @@ export async function build( isLastLocale, }); } catch (err) { + // TODO use error cause, but "docusaurus.mjs" does not print causes atm :s logger.error`Unable to build website for locale name=${locale}.`; throw err; } diff --git a/packages/docusaurus/src/commands/start.ts b/packages/docusaurus/src/commands/start.ts index 86440fd2d286..9a12e15eef45 100644 --- a/packages/docusaurus/src/commands/start.ts +++ b/packages/docusaurus/src/commands/start.ts @@ -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) => { + 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 = { diff --git a/packages/docusaurus/src/webpack/client.ts b/packages/docusaurus/src/webpack/client.ts index cd2d59720a32..86274d1b6991 100644 --- a/packages/docusaurus/src/webpack/client.ts +++ b/packages/docusaurus/src/webpack/client.ts @@ -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); } diff --git a/packages/docusaurus/src/webpack/plugins/LogPlugin.ts b/packages/docusaurus/src/webpack/plugins/LogPlugin.ts deleted file mode 100644 index a1703d2bcac6..000000000000 --- a/packages/docusaurus/src/webpack/plugins/LogPlugin.ts +++ /dev/null @@ -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); - } - } - }); - } -} diff --git a/packages/docusaurus/src/webpack/server.ts b/packages/docusaurus/src/webpack/server.ts index 4942ed5c0c76..adf7f6df7055 100644 --- a/packages/docusaurus/src/webpack/server.ts +++ b/packages/docusaurus/src/webpack/server.ts @@ -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', }), diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index 1f033a29dc14..0bc5e360d7e0 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -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 | 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 | 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 { // 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) => { diff --git a/website/_dogfooding/_pages tests/crashTest.tsx b/website/_dogfooding/_pages tests/crashTest.tsx new file mode 100644 index 000000000000..462a680fb1a5 --- /dev/null +++ b/website/_dogfooding/_pages tests/crashTest.tsx @@ -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 ( + + {/* eslint-disable-next-line @docusaurus/prefer-docusaurus-heading */} +

This crash if customFields.crashTest = true

+ +
+ ); +} diff --git a/website/_dogfooding/_pages tests/index.mdx b/website/_dogfooding/_pages tests/index.mdx index 545e2184c806..b5d1255f4861 100644 --- a/website/_dogfooding/_pages tests/index.mdx +++ b/website/_dogfooding/_pages tests/index.mdx @@ -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) diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 8642c6130cae..b8039c63efac 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -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.', From 109c5bc48d61e4004555d97a09a58fab68b7bde4 Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 7 Apr 2023 18:16:24 +0200 Subject: [PATCH 2/3] fix webpackbar pwa --- packages/docusaurus-plugin-pwa/package.json | 1 + packages/docusaurus-plugin-pwa/src/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-plugin-pwa/package.json b/packages/docusaurus-plugin-pwa/package.json index 9810dbba7947..de626bda1cb7 100644 --- a/packages/docusaurus-plugin-pwa/package.json +++ b/packages/docusaurus-plugin-pwa/package.json @@ -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" diff --git a/packages/docusaurus-plugin-pwa/src/index.ts b/packages/docusaurus-plugin-pwa/src/index.ts index 895dc56f4013..b3c8a4e7e670 100644 --- a/packages/docusaurus-plugin-pwa/src/index.ts +++ b/packages/docusaurus-plugin-pwa/src/index.ts @@ -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', }), From 09acbb3300dc3d74e6d076adc5e5dc957c8924aa Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Fri, 7 Apr 2023 18:42:41 +0200 Subject: [PATCH 3/3] use native error logging in CLI + polish --- packages/docusaurus/bin/docusaurus.mjs | 9 ++++----- packages/docusaurus/src/client/serverEntry.tsx | 5 +++-- packages/docusaurus/src/commands/build.ts | 9 ++++++--- packages/docusaurus/src/webpack/utils.ts | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/docusaurus/bin/docusaurus.mjs b/packages/docusaurus/bin/docusaurus.mjs index c631379197d2..4016fbd30bab 100755 --- a/packages/docusaurus/bin/docusaurus.mjs +++ b/packages/docusaurus/bin/docusaurus.mjs @@ -244,11 +244,10 @@ if (!process.argv.slice(2).length) { cli.parse(process.argv); process.on('unhandledRejection', (err) => { - // TODO this logger thing is annoying - // This makes it impossible to use native logging of nested stacktraces - // ie the error causes are not logged here - // It seems we can't have colors + nested stacktrace logging at the same time - 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}`; diff --git a/packages/docusaurus/src/client/serverEntry.tsx b/packages/docusaurus/src/client/serverEntry.tsx index 628ec5da7cd8..975cb15a468a 100644 --- a/packages/docusaurus/src/client/serverEntry.tsx +++ b/packages/docusaurus/src/client/serverEntry.tsx @@ -44,7 +44,7 @@ function buildSSRErrorMessage({ pathname: string; }): string { const parts = [ - `[ERROR] Docusaurus server-side rendering could not render static page with path ${pathname} because of error: ${error.message}`, + `Docusaurus server-side rendering could not render static page with path ${pathname} because of error: ${error.message}`, ]; const isNotDefinedErrorRegex = @@ -171,7 +171,8 @@ async function doRender(locals: Locals & {path: string}) { }); } catch (err) { // prettier-ignore - console.error(`[ERROR] Minification of page ${locals.path} failed.`); + console.error(`Minification of page ${locals.path} failed.`); + console.error(err); throw err; } } diff --git a/packages/docusaurus/src/commands/build.ts b/packages/docusaurus/src/commands/build.ts index 4fa4a4fdfe75..f79912cc545b 100644 --- a/packages/docusaurus/src/commands/build.ts +++ b/packages/docusaurus/src/commands/build.ts @@ -72,9 +72,12 @@ export async function build( isLastLocale, }); } catch (err) { - // TODO use error cause, but "docusaurus.mjs" does not print causes atm :s - 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({ diff --git a/packages/docusaurus/src/webpack/utils.ts b/packages/docusaurus/src/webpack/utils.ts index 0bc5e360d7e0..868182479dcc 100644 --- a/packages/docusaurus/src/webpack/utils.ts +++ b/packages/docusaurus/src/webpack/utils.ts @@ -277,7 +277,7 @@ export function compile(config: Configuration[]): Promise { const statsErrorMessage = formatStatsErrorMessage(errorsWarnings); reject( new Error( - `Failed to compile due to Webpack errors.\n${statsErrorMessage}`, + `Failed to compile due to Webpack errors.\n${statsErrorMessage}`, ), ); }