From 4b37c01a18a6832f8dd2fa1b248aafa1fd35609a Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 23 Mar 2023 15:22:46 +0100 Subject: [PATCH] polish(theme): better error messages on navbar item rendering failures + ErrorCauseBoundary API (#8735) Co-authored-by: sebastienlorber --- .../src/theme/Navbar/Content/index.tsx | 15 +++++++-- packages/docusaurus-theme-common/package.json | 1 + packages/docusaurus-theme-common/src/index.ts | 1 + .../src/utils/docsUtils.tsx | 8 ++--- .../src/utils/errorBoundaryUtils.tsx | 32 +++++++++++++++++-- .../src/__tests__/errorUtils.test.ts | 26 +++++++++++++++ .../docusaurus-utils-common/src/errorUtils.ts | 14 ++++++++ packages/docusaurus-utils-common/src/index.ts | 1 + .../src/client/theme-fallback/Error/index.tsx | 9 +++++- 9 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 packages/docusaurus-utils-common/src/__tests__/errorUtils.test.ts create mode 100644 packages/docusaurus-utils-common/src/errorUtils.ts diff --git a/packages/docusaurus-theme-classic/src/theme/Navbar/Content/index.tsx b/packages/docusaurus-theme-classic/src/theme/Navbar/Content/index.tsx index e499198ecabf..ddc0972c67ff 100644 --- a/packages/docusaurus-theme-classic/src/theme/Navbar/Content/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Navbar/Content/index.tsx @@ -6,7 +6,7 @@ */ import React, {type ReactNode} from 'react'; -import {useThemeConfig} from '@docusaurus/theme-common'; +import {useThemeConfig, ErrorCauseBoundary} from '@docusaurus/theme-common'; import { splitNavbarItems, useNavbarMobileSidebar, @@ -29,7 +29,18 @@ function NavbarItems({items}: {items: NavbarItemConfig[]}): JSX.Element { return ( <> {items.map((item, i) => ( - + + new Error( + `A theme navbar item failed to render. +Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config: +${JSON.stringify(item, null, 2)}`, + {cause: error}, + ) + }> + + ))} ); diff --git a/packages/docusaurus-theme-common/package.json b/packages/docusaurus-theme-common/package.json index 228b473d2435..61ab747f4774 100644 --- a/packages/docusaurus-theme-common/package.json +++ b/packages/docusaurus-theme-common/package.json @@ -36,6 +36,7 @@ "@docusaurus/plugin-content-docs": "2.3.1", "@docusaurus/plugin-content-pages": "2.3.1", "@docusaurus/utils": "2.3.1", + "@docusaurus/utils-common": "2.3.1", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 3b8e6e7df28d..d7993ab76ce5 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -93,4 +93,5 @@ export { export { ErrorBoundaryTryAgainButton, ErrorBoundaryError, + ErrorCauseBoundary, } from './utils/errorBoundaryUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index 726946be9e6c..1d03b8dc814c 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -271,8 +271,8 @@ export function useLayoutDocsSidebar( `Can't find any sidebar with id "${sidebarId}" in version${ versions.length > 1 ? 's' : '' } ${versions.map((version) => version.name).join(', ')}". - Available sidebar ids are: - - ${Object.keys(allSidebars).join('\n- ')}`, +Available sidebar ids are: +- ${Object.keys(allSidebars).join('\n- ')}`, ); } return sidebarEntry[1]; @@ -304,9 +304,9 @@ export function useLayoutDoc( return null; } throw new Error( - `DocNavbarItem: couldn't find any doc with id "${docId}" in version${ + `Couldn't find any doc with id "${docId}" in version${ versions.length > 1 ? 's' : '' - } ${versions.map((version) => version.name).join(', ')}". + } "${versions.map((version) => version.name).join(', ')}". Available doc ids are: - ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`, ); diff --git a/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx b/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx index 193ec7cd4ff7..051dbe86eb35 100644 --- a/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx @@ -7,6 +7,7 @@ import React, {type ComponentProps} from 'react'; import Translate from '@docusaurus/Translate'; +import {getErrorCausalChain} from '@docusaurus/utils-common'; import styles from './errorBoundaryUtils.module.css'; export function ErrorBoundaryTryAgainButton( @@ -22,7 +23,34 @@ export function ErrorBoundaryTryAgainButton( ); } - export function ErrorBoundaryError({error}: {error: Error}): JSX.Element { - return

{error.message}

; + const causalChain = getErrorCausalChain(error); + const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n'); + return

{fullMessage}

; +} + +/** + * This component is useful to wrap a low-level error into a more meaningful + * error with extra context, using the ES error-cause feature. + * + * new Error("extra context message",{cause: error})} + * > + * + * + */ +export class ErrorCauseBoundary extends React.Component< + { + children: React.ReactNode; + onError: (error: Error, errorInfo: React.ErrorInfo) => Error; + }, + unknown +> { + override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): never { + throw this.props.onError(error, errorInfo); + } + + override render(): React.ReactNode { + return this.props.children; + } } diff --git a/packages/docusaurus-utils-common/src/__tests__/errorUtils.test.ts b/packages/docusaurus-utils-common/src/__tests__/errorUtils.test.ts new file mode 100644 index 000000000000..1d29f660861f --- /dev/null +++ b/packages/docusaurus-utils-common/src/__tests__/errorUtils.test.ts @@ -0,0 +1,26 @@ +/** + * 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 {getErrorCausalChain} from '../errorUtils'; + +describe('getErrorCausalChain', () => { + it('works for simple error', () => { + const error = new Error('msg'); + expect(getErrorCausalChain(error)).toEqual([error]); + }); + + it('works for nested errors', () => { + const error = new Error('msg', { + cause: new Error('msg', {cause: new Error('msg')}), + }); + expect(getErrorCausalChain(error)).toEqual([ + error, + error.cause, + (error.cause as Error).cause, + ]); + }); +}); diff --git a/packages/docusaurus-utils-common/src/errorUtils.ts b/packages/docusaurus-utils-common/src/errorUtils.ts new file mode 100644 index 000000000000..5e3161dc47c2 --- /dev/null +++ b/packages/docusaurus-utils-common/src/errorUtils.ts @@ -0,0 +1,14 @@ +/** + * 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. + */ +type CausalChain = [Error, ...Error[]]; + +export function getErrorCausalChain(error: Error): CausalChain { + if (error.cause) { + return [error, ...getErrorCausalChain(error.cause as Error)]; + } + return [error]; +} diff --git a/packages/docusaurus-utils-common/src/index.ts b/packages/docusaurus-utils-common/src/index.ts index 11b925a28302..b1bbfb5237e1 100644 --- a/packages/docusaurus-utils-common/src/index.ts +++ b/packages/docusaurus-utils-common/src/index.ts @@ -10,3 +10,4 @@ export { default as applyTrailingSlash, type ApplyTrailingSlashParams, } from './applyTrailingSlash'; +export {getErrorCausalChain} from './errorUtils'; diff --git a/packages/docusaurus/src/client/theme-fallback/Error/index.tsx b/packages/docusaurus/src/client/theme-fallback/Error/index.tsx index 645bd1682bdf..79ace57d7627 100644 --- a/packages/docusaurus/src/client/theme-fallback/Error/index.tsx +++ b/packages/docusaurus/src/client/theme-fallback/Error/index.tsx @@ -11,6 +11,7 @@ import React from 'react'; import Head from '@docusaurus/Head'; import ErrorBoundary from '@docusaurus/ErrorBoundary'; +import {getErrorCausalChain} from '@docusaurus/utils-common'; import Layout from '@theme/Layout'; import type {Props} from '@theme/Error'; @@ -42,11 +43,17 @@ function ErrorDisplay({error, tryAgain}: Props): JSX.Element { }}> Try again -

{error.message}

+ ); } +function ErrorBoundaryError({error}: {error: Error}): JSX.Element { + const causalChain = getErrorCausalChain(error); + const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n'); + return

{fullMessage}

; +} + export default function Error({error, tryAgain}: Props): JSX.Element { // We wrap the error in its own error boundary because the layout can actually // throw too... Only the ErrorDisplay component is simple enough to be