From 4d4de0bb836953bf7f7ece11eb94fb8d0beea87d Mon Sep 17 00:00:00 2001 From: Tanner Date: Sun, 5 Mar 2023 01:00:26 -0800 Subject: [PATCH 1/5] refactor: update DocNavbarItem error message --- packages/docusaurus-theme-common/src/utils/docsUtils.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index 67b27265969b..d19f180ec7fb 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -345,7 +345,9 @@ export function useLayoutDoc( throw new Error( `DocNavbarItem: 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(', ')}". +Check themeConfig.navbar.items in your Docusaurus config to make sure +navbar items aren't attempting to reference a non-existent doc. Available doc ids are: - ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`, ); From aed9ddc96835088d391e94003fec1c2c9ae86c31 Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 8 Mar 2023 19:25:45 -0800 Subject: [PATCH 2/5] refactor: update DocNavbarItem for better error handling --- .../src/theme/NavbarItem/DocNavbarItem.tsx | 21 ++++++++++++++++++- .../src/utils/docsUtils.tsx | 2 -- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx index af745fd3a001..d05f9356990c 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx @@ -10,6 +10,25 @@ import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client'; import {useLayoutDoc} from '@docusaurus/theme-common/internal'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import type {Props} from '@theme/NavbarItem/DocNavbarItem'; +import type {GlobalDoc} from '@docusaurus/plugin-content-docs/client'; + +function useNavbarItemDoc( + docId: string, + docsPluginId?: string, +): GlobalDoc | null { + try { + return useLayoutDoc(docId, docsPluginId); + } catch (e) { + throw new Error( + [ + "There's a problem in a navbar item, check themeConfig.navbar.items", + "in your Docusaurus config to make sure you aren't attempting to", + 'reference a non-existent doc.', + ].join(' '), + {cause: e}, + ); + } +} export default function DocNavbarItem({ docId, @@ -18,7 +37,7 @@ export default function DocNavbarItem({ ...props }: Props): JSX.Element | null { const {activeDoc} = useActiveDocContext(docsPluginId); - const doc = useLayoutDoc(docId, docsPluginId); + const doc = useNavbarItemDoc(docId, docsPluginId); const pageActive = activeDoc?.path === doc?.path; // Draft and unlisted items are not displayed in the navbar. diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index d19f180ec7fb..b0c36641ac01 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -346,8 +346,6 @@ export function useLayoutDoc( `DocNavbarItem: couldn't find any doc with id "${docId}" in version${ versions.length > 1 ? 's' : '' } "${versions.map((version) => version.name).join(', ')}". -Check themeConfig.navbar.items in your Docusaurus config to make sure -navbar items aren't attempting to reference a non-existent doc. Available doc ids are: - ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`, ); From 998d7635042992f66b0d758a11f113d325a08aec Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 8 Mar 2023 19:31:03 -0800 Subject: [PATCH 3/5] refactor: use single quotes for useNavbarItemDoc error message From 9ede42a51a7b75269b2bddda062245898bc345ea Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 9 Mar 2023 17:55:27 +0100 Subject: [PATCH 4/5] error boundaries should render error causal chains --- .../src/theme/NavbarItem/DocNavbarItem.tsx | 6 +---- packages/docusaurus-theme-common/package.json | 1 + .../src/utils/docsUtils.tsx | 2 +- .../src/utils/errorBoundaryUtils.tsx | 6 +++-- .../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 ++++++- 8 files changed, 56 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/NavbarItem/DocNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx index d05f9356990c..0fcebfe70bb1 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx @@ -20,11 +20,7 @@ function useNavbarItemDoc( return useLayoutDoc(docId, docsPluginId); } catch (e) { throw new Error( - [ - "There's a problem in a navbar item, check themeConfig.navbar.items", - "in your Docusaurus config to make sure you aren't attempting to", - 'reference a non-existent doc.', - ].join(' '), + "There's a problem in a theme navbar item.\nPlease check themeConfig.navbar.items in your Docusaurus config to make sure you aren't attempting to reference a non-existent doc.", {cause: e}, ); } diff --git a/packages/docusaurus-theme-common/package.json b/packages/docusaurus-theme-common/package.json index c716b4b5d6b3..94145ba8407c 100644 --- a/packages/docusaurus-theme-common/package.json +++ b/packages/docusaurus-theme-common/package.json @@ -36,6 +36,7 @@ "@docusaurus/plugin-content-docs": "^3.0.0-alpha.0", "@docusaurus/plugin-content-pages": "^3.0.0-alpha.0", "@docusaurus/utils": "^3.0.0-alpha.0", + "@docusaurus/utils-common": "^3.0.0-alpha.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", diff --git a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx index b0c36641ac01..f775ecb02c87 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -343,7 +343,7 @@ 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(', ')}". Available doc ids are: diff --git a/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx b/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx index 193ec7cd4ff7..7143c6ff6281 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,8 @@ 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}

; } 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 From fe2370e5b20ef5b747a00c3ba2a880639eec096d Mon Sep 17 00:00:00 2001 From: sebastienlorber Date: Thu, 9 Mar 2023 18:30:39 +0100 Subject: [PATCH 5/5] Add ErrorCauseBoundary --- .../src/theme/Navbar/Content/index.tsx | 15 +++++++++-- .../src/theme/NavbarItem/DocNavbarItem.tsx | 17 +----------- packages/docusaurus-theme-common/src/index.ts | 1 + .../src/utils/docsUtils.tsx | 4 +-- .../src/utils/errorBoundaryUtils.tsx | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 20 deletions(-) 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-classic/src/theme/NavbarItem/DocNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx index 0fcebfe70bb1..af745fd3a001 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DocNavbarItem.tsx @@ -10,21 +10,6 @@ import {useActiveDocContext} from '@docusaurus/plugin-content-docs/client'; import {useLayoutDoc} from '@docusaurus/theme-common/internal'; import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem'; import type {Props} from '@theme/NavbarItem/DocNavbarItem'; -import type {GlobalDoc} from '@docusaurus/plugin-content-docs/client'; - -function useNavbarItemDoc( - docId: string, - docsPluginId?: string, -): GlobalDoc | null { - try { - return useLayoutDoc(docId, docsPluginId); - } catch (e) { - throw new Error( - "There's a problem in a theme navbar item.\nPlease check themeConfig.navbar.items in your Docusaurus config to make sure you aren't attempting to reference a non-existent doc.", - {cause: e}, - ); - } -} export default function DocNavbarItem({ docId, @@ -33,7 +18,7 @@ export default function DocNavbarItem({ ...props }: Props): JSX.Element | null { const {activeDoc} = useActiveDocContext(docsPluginId); - const doc = useNavbarItemDoc(docId, docsPluginId); + const doc = useLayoutDoc(docId, docsPluginId); const pageActive = activeDoc?.path === doc?.path; // Draft and unlisted items are not displayed in the navbar. diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 6fa4b65191a0..600c75778ce2 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -99,4 +99,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 f775ecb02c87..d0d8f6f93feb 100644 --- a/packages/docusaurus-theme-common/src/utils/docsUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/docsUtils.tsx @@ -310,8 +310,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]; diff --git a/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx b/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx index 7143c6ff6281..051dbe86eb35 100644 --- a/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx @@ -28,3 +28,29 @@ export function ErrorBoundaryError({error}: {error: Error}): JSX.Element { 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; + } +}