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:
Patrik Oldsberg
2020-12-17 17:20:24 +01:00
parent adb1afdc2b
commit c35554dc4c
7 changed files with 136 additions and 64 deletions
+9 -8
View File
@@ -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,
});
+41 -13
View File
@@ -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`);
});
});
+46 -27
View File
@@ -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', () => {
+1 -2
View File
@@ -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,
}),
);