polish(theme): better error messages on navbar item rendering failures + ErrorCauseBoundary API (#8735)

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
Tanner Dolby 2023-03-09 10:56:21 -07:00 committed by GitHub
parent 7961c5b8d5
commit ea2b13ea94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 98 additions and 9 deletions

View file

@ -6,7 +6,7 @@
*/ */
import React, {type ReactNode} from 'react'; import React, {type ReactNode} from 'react';
import {useThemeConfig} from '@docusaurus/theme-common'; import {useThemeConfig, ErrorCauseBoundary} from '@docusaurus/theme-common';
import { import {
splitNavbarItems, splitNavbarItems,
useNavbarMobileSidebar, useNavbarMobileSidebar,
@ -29,7 +29,18 @@ function NavbarItems({items}: {items: NavbarItemConfig[]}): JSX.Element {
return ( return (
<> <>
{items.map((item, i) => ( {items.map((item, i) => (
<NavbarItem {...item} key={i} /> <ErrorCauseBoundary
key={i}
onError={(error) =>
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},
)
}>
<NavbarItem {...item} />
</ErrorCauseBoundary>
))} ))}
</> </>
); );

View file

@ -36,6 +36,7 @@
"@docusaurus/plugin-content-docs": "^3.0.0-alpha.0", "@docusaurus/plugin-content-docs": "^3.0.0-alpha.0",
"@docusaurus/plugin-content-pages": "^3.0.0-alpha.0", "@docusaurus/plugin-content-pages": "^3.0.0-alpha.0",
"@docusaurus/utils": "^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/history": "^4.7.11",
"@types/react": "*", "@types/react": "*",
"@types/react-router-config": "*", "@types/react-router-config": "*",

View file

@ -99,4 +99,5 @@ export {
export { export {
ErrorBoundaryTryAgainButton, ErrorBoundaryTryAgainButton,
ErrorBoundaryError, ErrorBoundaryError,
ErrorCauseBoundary,
} from './utils/errorBoundaryUtils'; } from './utils/errorBoundaryUtils';

View file

@ -310,8 +310,8 @@ export function useLayoutDocsSidebar(
`Can't find any sidebar with id "${sidebarId}" in version${ `Can't find any sidebar with id "${sidebarId}" in version${
versions.length > 1 ? 's' : '' versions.length > 1 ? 's' : ''
} ${versions.map((version) => version.name).join(', ')}". } ${versions.map((version) => version.name).join(', ')}".
Available sidebar ids are: Available sidebar ids are:
- ${Object.keys(allSidebars).join('\n- ')}`, - ${Object.keys(allSidebars).join('\n- ')}`,
); );
} }
return sidebarEntry[1]; return sidebarEntry[1];
@ -343,9 +343,9 @@ export function useLayoutDoc(
return null; return null;
} }
throw new Error( 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.length > 1 ? 's' : ''
} ${versions.map((version) => version.name).join(', ')}". } "${versions.map((version) => version.name).join(', ')}".
Available doc ids are: Available doc ids are:
- ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`, - ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`,
); );

View file

@ -7,6 +7,7 @@
import React, {type ComponentProps} from 'react'; import React, {type ComponentProps} from 'react';
import Translate from '@docusaurus/Translate'; import Translate from '@docusaurus/Translate';
import {getErrorCausalChain} from '@docusaurus/utils-common';
import styles from './errorBoundaryUtils.module.css'; import styles from './errorBoundaryUtils.module.css';
export function ErrorBoundaryTryAgainButton( export function ErrorBoundaryTryAgainButton(
@ -22,7 +23,34 @@ export function ErrorBoundaryTryAgainButton(
</button> </button>
); );
} }
export function ErrorBoundaryError({error}: {error: Error}): JSX.Element { export function ErrorBoundaryError({error}: {error: Error}): JSX.Element {
return <p className={styles.errorBoundaryError}>{error.message}</p>; const causalChain = getErrorCausalChain(error);
const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n');
return <p className={styles.errorBoundaryError}>{fullMessage}</p>;
}
/**
* This component is useful to wrap a low-level error into a more meaningful
* error with extra context, using the ES error-cause feature.
*
* <ErrorCauseBoundary
* onError={(error) => new Error("extra context message",{cause: error})}
* >
* <RiskyComponent>
* </ErrorCauseBoundary>
*/
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;
}
} }

View file

@ -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,
]);
});
});

View file

@ -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];
}

View file

@ -10,3 +10,4 @@ export {
default as applyTrailingSlash, default as applyTrailingSlash,
type ApplyTrailingSlashParams, type ApplyTrailingSlashParams,
} from './applyTrailingSlash'; } from './applyTrailingSlash';
export {getErrorCausalChain} from './errorUtils';

View file

@ -11,6 +11,7 @@
import React from 'react'; import React from 'react';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import ErrorBoundary from '@docusaurus/ErrorBoundary'; import ErrorBoundary from '@docusaurus/ErrorBoundary';
import {getErrorCausalChain} from '@docusaurus/utils-common';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import type {Props} from '@theme/Error'; import type {Props} from '@theme/Error';
@ -42,11 +43,17 @@ function ErrorDisplay({error, tryAgain}: Props): JSX.Element {
}}> }}>
Try again Try again
</button> </button>
<p style={{whiteSpace: 'pre-wrap'}}>{error.message}</p> <ErrorBoundaryError error={error} />
</div> </div>
); );
} }
function ErrorBoundaryError({error}: {error: Error}): JSX.Element {
const causalChain = getErrorCausalChain(error);
const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n');
return <p style={{whiteSpace: 'pre-wrap'}}>{fullMessage}</p>;
}
export default function Error({error, tryAgain}: Props): JSX.Element { export default function Error({error, tryAgain}: Props): JSX.Element {
// We wrap the error in its own error boundary because the layout can actually // 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 // throw too... Only the ErrorDisplay component is simple enough to be