diff --git a/.changeset/good-glasses-build.md b/.changeset/good-glasses-build.md new file mode 100644 index 0000000000..3d506d3e43 --- /dev/null +++ b/.changeset/good-glasses-build.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Add support for discovering plugins through the app element tree, removing the need to register them explicitly. diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index c91eef1fb0..3cade002de 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -212,6 +212,9 @@ describe('Integration Test', () => { expect(screen.getByText('extLink2: /foo/a')).toBeInTheDocument(); expect(screen.getByText('extLink3: /sub1')).toBeInTheDocument(); expect(screen.getByText('extLink4: /foo/b')).toBeInTheDocument(); + + // Plugins should be discovered through element tree + expect(app.getPlugins()).toEqual([plugin1, plugin2]); }); it('runs happy paths without optional routes', async () => { diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index d2346e478c..fdbee075fc 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -52,6 +52,7 @@ import { } from '../extensions/traversal'; import { IconComponent, IconComponentMap, IconKey } from '../icons'; import { BackstagePlugin } from '../plugin'; +import { pluginCollector } from '../plugin/collectors'; import { AnyRoutes } from '../plugin/types'; import { RouteRef, ExternalRouteRef, SubRouteRef } from '../routing'; import { @@ -189,7 +190,7 @@ export class PrivateAppImpl implements BackstageApp { private readonly apis: Iterable; private readonly icons: IconComponentMap; - private readonly plugins: BackstagePlugin[]; + private readonly plugins: Set>; private readonly components: AppComponents; private readonly themes: AppTheme[]; private readonly configLoader?: AppConfigLoader; @@ -201,7 +202,7 @@ export class PrivateAppImpl implements BackstageApp { constructor(options: FullAppOptions) { this.apis = options.apis; this.icons = options.icons; - this.plugins = options.plugins; + this.plugins = new Set(options.plugins); this.components = options.components; this.themes = options.themes; this.configLoader = options.configLoader; @@ -210,7 +211,7 @@ export class PrivateAppImpl implements BackstageApp { } getPlugins(): BackstagePlugin[] { - return this.plugins; + return Array.from(this.plugins); } getSystemIcon(key: IconKey): IconComponent | undefined { @@ -276,7 +277,6 @@ export class PrivateAppImpl implements BackstageApp { getProvider(): ComponentType<{}> { const appContext = new AppContextImpl(this); - const apiHolder = this.getApiHolder(); const Provider = ({ children }: PropsWithChildren<{}>) => { const appThemeApi = useMemo( @@ -292,11 +292,25 @@ export class PrivateAppImpl implements BackstageApp { routePaths: routePathCollector, routeParents: routeParentCollector, routeObjects: routeObjectCollector, + collectedPlugins: pluginCollector, }, }); validateRoutes(result.routePaths, result.routeParents); + // TODO(Rugvip): Restructure the public API so that we can get an immediate view of + // the app, rather than having to wait for the provider to render. + // For now we need to push the additional plugins we find during + // collection and then make sure we initialize things afterwards. + result.collectedPlugins.forEach(plugin => this.plugins.add(plugin)); + this.verifyPlugins(this.plugins); + + // Initialize APIs once all plugins are available + if (this.apiHolder) { + throw new Error('Plugin holder was initialized too soon'); + } + this.getApiHolder(); + return result; }, [children]); @@ -340,7 +354,7 @@ export class PrivateAppImpl implements BackstageApp { } return ( - + ) { const pluginIds = new Set(); - for (const plugin of this.plugins) { + for (const plugin of plugins) { const id = plugin.getId(); if (pluginIds.has(id)) { throw new Error(`Duplicate plugin found '${id}'`);