mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 18:27:56 +02:00
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:
parent
7961c5b8d5
commit
ea2b13ea94
9 changed files with 98 additions and 9 deletions
|
@ -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) => (
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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": "*",
|
||||
|
|
|
@ -99,4 +99,5 @@ export {
|
|||
export {
|
||||
ErrorBoundaryTryAgainButton,
|
||||
ErrorBoundaryError,
|
||||
ErrorCauseBoundary,
|
||||
} from './utils/errorBoundaryUtils';
|
||||
|
|
|
@ -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];
|
||||
|
@ -343,9 +343,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- ')}`,
|
||||
);
|
||||
|
|
|
@ -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(
|
|||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
14
packages/docusaurus-utils-common/src/errorUtils.ts
Normal file
14
packages/docusaurus-utils-common/src/errorUtils.ts
Normal 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];
|
||||
}
|
|
@ -10,3 +10,4 @@ export {
|
|||
default as applyTrailingSlash,
|
||||
type ApplyTrailingSlashParams,
|
||||
} from './applyTrailingSlash';
|
||||
export {getErrorCausalChain} from './errorUtils';
|
||||
|
|
|
@ -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
|
||||
</button>
|
||||
<p style={{whiteSpace: 'pre-wrap'}}>{error.message}</p>
|
||||
<ErrorBoundaryError error={error} />
|
||||
</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 {
|
||||
// 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
|
||||
|
|
Loading…
Add table
Reference in a new issue