core-plugin-api: avoid using BootErrorPage
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-plugin-api': patch
|
||||
---
|
||||
|
||||
Failure to lazy load an extension will now always result in an error being thrown to be forwarded to error boundaries, rather than being rendered using the `BootErrorPage` app component.
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useAnalyticsContext } from '../analytics/AnalyticsContext';
|
||||
import { useApp, ErrorBoundaryFallbackProps } from '../app';
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
createReactExtension,
|
||||
createRoutableExtension,
|
||||
} from './extensions';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
|
||||
jest.mock('../app');
|
||||
|
||||
@@ -120,6 +121,48 @@ describe('extensions', () => {
|
||||
expect(errors[0]).toMatchObject({ detail: new Error('Test error') });
|
||||
});
|
||||
|
||||
it('should handle failed lazy loads', async () => {
|
||||
const BrokenComponent = plugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'BrokenComponent',
|
||||
component: {
|
||||
lazy: async () => {
|
||||
if (true as boolean) {
|
||||
throw new Error('Test error');
|
||||
}
|
||||
return () => <div />;
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
mocked(useApp).mockReturnValue({
|
||||
getComponents: () => ({
|
||||
Progress: () => null,
|
||||
ErrorBoundaryFallback: (props: ErrorBoundaryFallbackProps) => (
|
||||
<>
|
||||
Error in {props.plugin?.getId()}: {String(props.error)}
|
||||
</>
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const { error: errors } = await withLogCollector(['error'], async () => {
|
||||
await act(async () => {
|
||||
render(<BrokenComponent />);
|
||||
});
|
||||
});
|
||||
screen.getByText(
|
||||
'Error in my-plugin: Error: Failed lazy loading of the BrokenComponent extension, try to reload the page; caused by Error: Test error',
|
||||
);
|
||||
expect(errors[0]).toMatchObject({
|
||||
detail: new ForwardedError(
|
||||
'Failed lazy loading of the BrokenComponent extension, try to reload the page',
|
||||
new Error('Test error'),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap extended component with analytics context', async () => {
|
||||
const AnalyticsSpyExtension = plugin.provide(
|
||||
createReactExtension({
|
||||
|
||||
@@ -22,6 +22,7 @@ import { attachComponentData } from './componentData';
|
||||
import { Extension, BackstagePlugin } from '../plugin';
|
||||
import { PluginErrorBoundary } from './PluginErrorBoundary';
|
||||
import { routableExtensionRenderedEvent } from '../analytics/Tracker';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* Lazy or synchronous retrieving of extension components.
|
||||
@@ -79,62 +80,51 @@ export function createRoutableExtension<
|
||||
return createReactExtension({
|
||||
component: {
|
||||
lazy: () =>
|
||||
component().then(
|
||||
InnerComponent => {
|
||||
const RoutableExtensionWrapper: any = (props: any) => {
|
||||
const analytics = useAnalytics();
|
||||
component().then(InnerComponent => {
|
||||
const RoutableExtensionWrapper: any = (props: any) => {
|
||||
const analytics = useAnalytics();
|
||||
|
||||
// Validate that the routing is wired up correctly in the App.tsx
|
||||
try {
|
||||
useRouteRef(mountPoint);
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
const { message } = error as { message?: unknown };
|
||||
if (
|
||||
typeof message === 'string' &&
|
||||
message.startsWith('No path for ')
|
||||
) {
|
||||
throw new Error(
|
||||
`Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
|
||||
'Routable extension components may not be rendered by other components and must be ' +
|
||||
'directly available as an element within the App provider component.',
|
||||
);
|
||||
}
|
||||
// Validate that the routing is wired up correctly in the App.tsx
|
||||
try {
|
||||
useRouteRef(mountPoint);
|
||||
} catch (error) {
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
const { message } = error as { message?: unknown };
|
||||
if (
|
||||
typeof message === 'string' &&
|
||||
message.startsWith('No path for ')
|
||||
) {
|
||||
throw new Error(
|
||||
`Routable extension component with mount point ${mountPoint} was not discovered in the app element tree. ` +
|
||||
'Routable extension components may not be rendered by other components and must be ' +
|
||||
'directly available as an element within the App provider component.',
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// This event, never exposed to end-users of the analytics API,
|
||||
// helps inform which extension metadata gets associated with a
|
||||
// navigation event when the route navigated to is a gathered
|
||||
// mountpoint.
|
||||
useEffect(() => {
|
||||
analytics.captureEvent(routableExtensionRenderedEvent, '');
|
||||
}, [analytics]);
|
||||
// This event, never exposed to end-users of the analytics API,
|
||||
// helps inform which extension metadata gets associated with a
|
||||
// navigation event when the route navigated to is a gathered
|
||||
// mountpoint.
|
||||
useEffect(() => {
|
||||
analytics.captureEvent(routableExtensionRenderedEvent, '');
|
||||
}, [analytics]);
|
||||
|
||||
return <InnerComponent {...props} />;
|
||||
};
|
||||
return <InnerComponent {...props} />;
|
||||
};
|
||||
|
||||
const componentName =
|
||||
name ||
|
||||
(InnerComponent as { displayName?: string }).displayName ||
|
||||
InnerComponent.name ||
|
||||
'LazyComponent';
|
||||
const componentName =
|
||||
name ||
|
||||
(InnerComponent as { displayName?: string }).displayName ||
|
||||
InnerComponent.name ||
|
||||
'LazyComponent';
|
||||
|
||||
RoutableExtensionWrapper.displayName = `RoutableExtension(${componentName})`;
|
||||
RoutableExtensionWrapper.displayName = `RoutableExtension(${componentName})`;
|
||||
|
||||
return RoutableExtensionWrapper as T;
|
||||
},
|
||||
error => {
|
||||
const RoutableExtensionWrapper: any = (_: any) => {
|
||||
const app = useApp();
|
||||
const { BootErrorPage } = app.getComponents();
|
||||
|
||||
return <BootErrorPage step="load-chunk" error={error} />;
|
||||
};
|
||||
return RoutableExtensionWrapper;
|
||||
},
|
||||
),
|
||||
return RoutableExtensionWrapper as T;
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
'core.mountPoint': mountPoint,
|
||||
@@ -225,7 +215,16 @@ export function createReactExtension<
|
||||
if ('lazy' in options.component) {
|
||||
const lazyLoader = options.component.lazy;
|
||||
Component = lazy(() =>
|
||||
lazyLoader().then(component => ({ default: component })),
|
||||
lazyLoader().then(
|
||||
component => ({ default: component }),
|
||||
error => {
|
||||
const ofExtension = name ? ` of the ${name} extension` : '';
|
||||
throw new ForwardedError(
|
||||
`Failed lazy loading${ofExtension}, try to reload the page`,
|
||||
error,
|
||||
);
|
||||
},
|
||||
),
|
||||
) as unknown as T;
|
||||
} else {
|
||||
Component = options.component.sync;
|
||||
|
||||
Reference in New Issue
Block a user