diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index 4050343bbf..8dd40d52b0 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -66,23 +66,24 @@ describe('Integration Test', () => { const HiddenComponent = plugin2.provide( createRoutableExtension({ - component: (_: { path?: string }) =>
, + component: () => Promise.resolve((_: { path?: string }) =>
), 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
Our Route Is: {routeRefFunction({})}
; - }, + component: () => + Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const routeRefFunction = useRouteRef(externalRouteRef); + return
Our Route Is: {routeRefFunction({})}
; + }), 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( diff --git a/packages/core-api/src/extensions/extensions.test.tsx b/packages/core-api/src/extensions/extensions.test.tsx index 861045fb3d..26755b3bcf 100644 --- a/packages/core-api/src/extensions/extensions.test.tsx +++ b/packages/core-api/src/extensions/extensions.test.tsx @@ -33,7 +33,9 @@ describe('extensions', () => { const Component = () =>
; 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, }); diff --git a/packages/core-api/src/extensions/extensions.tsx b/packages/core-api/src/extensions/extensions.tsx index aca29e87dc..adec690f20 100644 --- a/packages/core-api/src/extensions/extensions.tsx +++ b/packages/core-api/src/extensions/extensions.tsx @@ -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 = + | { + lazy: () => Promise; + } + | { + sync: T; + }; + export function createRoutableExtension< T extends (props: any) => JSX.Element ->(options: { component: T; mountPoint: RouteRef }): Extension { +>(options: { + component: () => Promise; + mountPoint: RouteRef; +}): Extension { 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 { +>(options: { component: ComponentLoader }): Extension { const { component } = options; return createReactExtension({ component }); } export function createReactExtension< T extends (props: any) => JSX.Element ->(options: { component: T; data?: Record }): Extension { +>(options: { + component: ComponentLoader; + data?: Record; +}): Extension { 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) { - const Result: any = (props: any) => ; + const Result: any = (props: any) => ( + + + + ); 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; }, }; diff --git a/packages/core-api/src/plugin/collectors.test.tsx b/packages/core-api/src/plugin/collectors.test.tsx index e5f8a123e3..5baf2539ab 100644 --- a/packages/core-api/src/plugin/collectors.test.tsx +++ b/packages/core-api/src/plugin/collectors.test.tsx @@ -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', () => { diff --git a/packages/core-api/src/routing/collectors.test.tsx b/packages/core-api/src/routing/collectors.test.tsx index e44474565c..200846f175 100644 --- a/packages/core-api/src/routing/collectors.test.tsx +++ b/packages/core-api/src/routing/collectors.test.tsx @@ -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`); }); }); diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index a29e263e14..3dbeaf8e3e 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -85,19 +85,34 @@ const MockRouteSource = (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 = ( @@ -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 = ( @@ -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 = ( - - - - - - - - + + + + + + + + + + ); 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', () => { diff --git a/plugins/graphiql/src/plugin.ts b/plugins/graphiql/src/plugin.ts index fb4c33760d..a0bb79ed29 100644 --- a/plugins/graphiql/src/plugin.ts +++ b/plugins/graphiql/src/plugin.ts @@ -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, }), );