core-api: add lazy loading of component extensions
Co-authored-by: Fredrik Adelöw <freben@gmail.com> Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
@@ -66,23 +66,24 @@ describe('Integration Test', () => {
|
||||
|
||||
const HiddenComponent = plugin2.provide(
|
||||
createRoutableExtension({
|
||||
component: (_: { path?: string }) => <div />,
|
||||
component: () => Promise.resolve((_: { path?: string }) => <div />),
|
||||
mountPoint: plugin2RouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
const ExposedComponent = plugin1.provide(
|
||||
createRoutableExtension({
|
||||
component: (_: PropsWithChildren<{ path?: string }>) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const routeRefFunction = useRouteRef(externalRouteRef);
|
||||
return <div>Our Route Is: {routeRefFunction({})}</div>;
|
||||
},
|
||||
component: () =>
|
||||
Promise.resolve((_: PropsWithChildren<{ path?: string }>) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const routeRefFunction = useRouteRef(externalRouteRef);
|
||||
return <div>Our Route Is: {routeRefFunction({})}</div>;
|
||||
}),
|
||||
mountPoint: plugin1RouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
it('runs happy path', () => {
|
||||
it('runs happy path', async () => {
|
||||
const components = {
|
||||
NotFoundErrorPage: () => null,
|
||||
BootErrorPage: () => null,
|
||||
@@ -112,7 +113,7 @@ describe('Integration Test', () => {
|
||||
const Provider = app.getProvider();
|
||||
const Router = app.getRouter();
|
||||
|
||||
renderWithEffects(
|
||||
await renderWithEffects(
|
||||
<Provider>
|
||||
<Router>
|
||||
<Routes>
|
||||
|
||||
@@ -33,7 +33,9 @@ describe('extensions', () => {
|
||||
const Component = () => <div />;
|
||||
|
||||
const extension = createReactExtension({
|
||||
component: Component,
|
||||
component: {
|
||||
sync: Component,
|
||||
},
|
||||
data: {
|
||||
myData: { foo: 'bar' },
|
||||
},
|
||||
@@ -51,11 +53,13 @@ describe('extensions', () => {
|
||||
const routeRef = createRouteRef({ path: '/foo', title: 'Foo' });
|
||||
|
||||
const extension1 = createComponentExtension({
|
||||
component: Component,
|
||||
component: {
|
||||
sync: Component,
|
||||
},
|
||||
});
|
||||
|
||||
const extension2 = createRoutableExtension({
|
||||
component: Component,
|
||||
component: () => Promise.resolve(Component),
|
||||
mountPoint: routeRef,
|
||||
});
|
||||
|
||||
|
||||
@@ -14,17 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { RouteRef } from '../routing';
|
||||
import { attachComponentData } from './componentData';
|
||||
import { Extension, BackstagePlugin } from '../plugin/types';
|
||||
|
||||
type ComponentLoader<T> =
|
||||
| {
|
||||
lazy: () => Promise<T>;
|
||||
}
|
||||
| {
|
||||
sync: T;
|
||||
};
|
||||
|
||||
export function createRoutableExtension<
|
||||
T extends (props: any) => JSX.Element
|
||||
>(options: { component: T; mountPoint: RouteRef }): Extension<T> {
|
||||
>(options: {
|
||||
component: () => Promise<T>;
|
||||
mountPoint: RouteRef;
|
||||
}): Extension<T> {
|
||||
const { component, mountPoint } = options;
|
||||
return createReactExtension({
|
||||
component,
|
||||
component: {
|
||||
lazy: component,
|
||||
},
|
||||
data: {
|
||||
'core.mountPoint': mountPoint,
|
||||
},
|
||||
@@ -33,32 +46,47 @@ export function createRoutableExtension<
|
||||
|
||||
export function createComponentExtension<
|
||||
T extends (props: any) => JSX.Element
|
||||
>(options: { component: T }): Extension<T> {
|
||||
>(options: { component: ComponentLoader<T> }): Extension<T> {
|
||||
const { component } = options;
|
||||
return createReactExtension({ component });
|
||||
}
|
||||
|
||||
export function createReactExtension<
|
||||
T extends (props: any) => JSX.Element
|
||||
>(options: { component: T; data?: Record<string, unknown> }): Extension<T> {
|
||||
>(options: {
|
||||
component: ComponentLoader<T>;
|
||||
data?: Record<string, unknown>;
|
||||
}): Extension<T> {
|
||||
const { data = {} } = options;
|
||||
const Component = options.component as T & {
|
||||
displayName?: string;
|
||||
};
|
||||
|
||||
let Component: T;
|
||||
if ('lazy' in options.component) {
|
||||
const lazyLoader = options.component.lazy;
|
||||
Component = (lazy(() =>
|
||||
lazyLoader().then(component => ({ default: component })),
|
||||
) as unknown) as T;
|
||||
} else {
|
||||
Component = options.component.sync;
|
||||
}
|
||||
const componentName =
|
||||
(Component as { displayName?: string }).displayName ||
|
||||
Component.name ||
|
||||
'Component';
|
||||
|
||||
return {
|
||||
expose(plugin: BackstagePlugin<any, any>) {
|
||||
const Result: any = (props: any) => <Component {...props} />;
|
||||
const Result: any = (props: any) => (
|
||||
<Suspense fallback="...">
|
||||
<Component {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
attachComponentData(Result, 'core.plugin', plugin);
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
attachComponentData(Result, key, value);
|
||||
}
|
||||
|
||||
const name = Component.displayName || Component.name || 'Component';
|
||||
if (name) {
|
||||
Result.displayName = `Extension(${name})`;
|
||||
}
|
||||
Result.displayName = `Extension(${componentName})`;
|
||||
return Result;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,19 +42,25 @@ const ref1 = createRouteRef(mockConfig());
|
||||
const ref2 = createRouteRef(mockConfig());
|
||||
|
||||
const Extension1 = pluginA.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref1 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref1,
|
||||
}),
|
||||
);
|
||||
const Extension2 = pluginB.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref2 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref2,
|
||||
}),
|
||||
);
|
||||
const Extension3 = pluginA.provide(
|
||||
createComponentExtension({ component: MockComponent }),
|
||||
createComponentExtension({ component: { sync: MockComponent } }),
|
||||
);
|
||||
const Extension4 = pluginB.provide(
|
||||
createComponentExtension({ component: MockComponent }),
|
||||
createComponentExtension({ component: { sync: MockComponent } }),
|
||||
);
|
||||
const Extension5 = pluginC.provide(
|
||||
createComponentExtension({ component: MockComponent }),
|
||||
createComponentExtension({ component: { sync: MockComponent } }),
|
||||
);
|
||||
|
||||
describe('collection', () => {
|
||||
|
||||
@@ -41,19 +41,34 @@ const ref4 = createRouteRef(mockConfig());
|
||||
const ref5 = createRouteRef(mockConfig());
|
||||
|
||||
const Extension1 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref1 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref1,
|
||||
}),
|
||||
);
|
||||
const Extension2 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref2 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref2,
|
||||
}),
|
||||
);
|
||||
const Extension3 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref3 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref3,
|
||||
}),
|
||||
);
|
||||
const Extension4 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref4 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref4,
|
||||
}),
|
||||
);
|
||||
const Extension5 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref5 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref5,
|
||||
}),
|
||||
);
|
||||
|
||||
describe('discovery', () => {
|
||||
@@ -190,6 +205,6 @@ describe('discovery', () => {
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
}),
|
||||
).toThrow(`Visited element Extension(MockComponent) twice`);
|
||||
).toThrow(`Visited element Extension(Component) twice`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,19 +85,34 @@ const MockRouteSource = <T extends { [name in string]: string }>(props: {
|
||||
};
|
||||
|
||||
const Extension1 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref1 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref1,
|
||||
}),
|
||||
);
|
||||
const Extension2 = plugin.provide(
|
||||
createRoutableExtension({ component: MockRouteSource, mountPoint: ref2 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockRouteSource),
|
||||
mountPoint: ref2,
|
||||
}),
|
||||
);
|
||||
const Extension3 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref3 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref3,
|
||||
}),
|
||||
);
|
||||
const Extension4 = plugin.provide(
|
||||
createRoutableExtension({ component: MockRouteSource, mountPoint: ref4 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockRouteSource),
|
||||
mountPoint: ref4,
|
||||
}),
|
||||
);
|
||||
const Extension5 = plugin.provide(
|
||||
createRoutableExtension({ component: MockComponent, mountPoint: ref5 }),
|
||||
createRoutableExtension({
|
||||
component: () => Promise.resolve(MockComponent),
|
||||
mountPoint: ref5,
|
||||
}),
|
||||
);
|
||||
|
||||
function withRoutingProvider(
|
||||
@@ -127,7 +142,7 @@ function withRoutingProvider(
|
||||
}
|
||||
|
||||
describe('discovery', () => {
|
||||
it('should handle simple routeRef path creation for routeRefs used in other parts of the app', () => {
|
||||
it('should handle simple routeRef path creation for routeRefs used in other parts of the app', async () => {
|
||||
const root = (
|
||||
<MemoryRouter initialEntries={['/foo/bar']}>
|
||||
<Routes>
|
||||
@@ -151,7 +166,9 @@ describe('discovery', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
expect(rendered.getByText('Path at inside: /foo/bar')).toBeInTheDocument();
|
||||
await expect(
|
||||
rendered.findByText('Path at inside: /foo/bar'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByText('Path at insideExternal: /baz'),
|
||||
).toBeInTheDocument();
|
||||
@@ -164,7 +181,7 @@ describe('discovery', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle routeRefs with parameters', () => {
|
||||
it('should handle routeRefs with parameters', async () => {
|
||||
const root = (
|
||||
<MemoryRouter initialEntries={['/foo/bar/wat']}>
|
||||
<Routes>
|
||||
@@ -187,37 +204,39 @@ describe('discovery', () => {
|
||||
|
||||
const rendered = render(withRoutingProvider(root));
|
||||
|
||||
expect(
|
||||
rendered.getByText('Path at inside: /foo/bar/bleb'),
|
||||
).toBeInTheDocument();
|
||||
await expect(
|
||||
rendered.findByText('Path at inside: /foo/bar/bleb'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(
|
||||
rendered.getByText('Path at outside: /foo/bar/blob'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle relative routing within parameterized routePaths', () => {
|
||||
it('should handle relative routing within parameterized routePaths', async () => {
|
||||
const root = (
|
||||
<MemoryRouter initialEntries={['/foo/blob/baz']}>
|
||||
<Routes>
|
||||
<Extension5 path="/foo/:id">
|
||||
<Extension2 path="/bar" name="inside" routeRef={ref3} />
|
||||
<Extension3 path="/baz" />
|
||||
</Extension5>
|
||||
</Routes>
|
||||
<MockRouteSource name="outsideNoParams" routeRef={ref3} />
|
||||
<MockRouteSource
|
||||
name="outsideWithParams"
|
||||
routeRef={ref3}
|
||||
params={{ id: 'blob' }}
|
||||
/>
|
||||
<React.Suspense fallback="loller">
|
||||
<Routes>
|
||||
<Extension5 path="/foo/:id">
|
||||
<Extension2 path="/bar" name="inside" routeRef={ref3} />
|
||||
<Extension3 path="/baz" />
|
||||
</Extension5>
|
||||
</Routes>
|
||||
<MockRouteSource name="outsideNoParams" routeRef={ref3} />
|
||||
<MockRouteSource
|
||||
name="outsideWithParams"
|
||||
routeRef={ref3}
|
||||
params={{ id: 'blob' }}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const rendered = render(withRoutingProvider(root));
|
||||
|
||||
expect(
|
||||
rendered.getByText('Path at inside: /foo/blob/baz'),
|
||||
).toBeInTheDocument();
|
||||
await expect(
|
||||
rendered.findByText('Path at inside: /foo/blob/baz'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should throw errors for routing to other routeRefs with unsupported parameters', () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
createApiFactory,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core';
|
||||
import { GraphiQLPage as Component } from './components';
|
||||
import { graphQlBrowseApiRef, GraphQLEndpoints } from './lib/api';
|
||||
import { graphiQLRouteRef } from './route-refs';
|
||||
|
||||
@@ -42,7 +41,7 @@ export const plugin = createPlugin({
|
||||
|
||||
export const GraphiQLPage = plugin.provide(
|
||||
createRoutableExtension({
|
||||
component: Component,
|
||||
component: () => import('./components').then(m => m.GraphiQLPage),
|
||||
mountPoint: graphiQLRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user