diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 08db759511..e7dd584734 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -24,9 +24,12 @@ export function createApp(options?: { }; // @public -export type CreateAppFeatureLoader = (options: { - config: ConfigApi; -}) => Promise; +export interface CreateAppFeatureLoader { + getLoaderName(): string; + load(options: { config: ConfigApi }): Promise<{ + features: FrontendFeature[]; + }>; +} // @public export type CreateAppRouteBinder = < diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 244ce625fb..dc39ab3ae5 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -25,7 +25,7 @@ import { createThemeExtension, } from '@backstage/frontend-plugin-api'; import { screen, waitFor } from '@testing-library/react'; -import { createApp } from './createApp'; +import { CreateAppFeatureLoader, createApp } from './createApp'; import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; @@ -100,23 +100,32 @@ describe('createApp', () => { }); it('should support feature loaders', async () => { + const loader: CreateAppFeatureLoader = { + getLoaderName() { + return 'test-loader'; + }, + async load({ config }) { + return { + features: [ + createPlugin({ + id: 'test', + extensions: [ + createPageExtension({ + defaultPath: '/', + loader: async () =>
{config.getString('key')}
, + }), + ], + }), + ], + }; + }, + }; + const app = createApp({ configLoader: async () => ({ config: new MockConfigApi({ key: 'config-value' }), }), - features: [ - async ({ config }) => [ - createPlugin({ - id: 'test', - extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
{config.getString('key')}
, - }), - ], - }), - ], - ], + features: [loader], }); await renderWithEffects(app.createRoot()); @@ -127,21 +136,26 @@ describe('createApp', () => { }); it('should propagate errors thrown by feature loaders', async () => { + const loader: CreateAppFeatureLoader = { + getLoaderName() { + return 'test-loader'; + }, + async load() { + throw new TypeError('boom'); + }, + }; + const app = createApp({ configLoader: async () => ({ config: new MockConfigApi({}), }), - features: [ - async () => { - throw new TypeError('boom'); - }, - ], + features: [loader], }); await expect( renderWithEffects(app.createRoot()), ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to read frontend features from loader, TypeError: boom"`, + `"Failed to read frontend features from loader 'test-loader', TypeError: boom"`, ); }); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index f3b32db399..572e88e647 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -244,9 +244,19 @@ function deduplicateFeatures( * * @public */ -export type CreateAppFeatureLoader = (options: { - config: ConfigApi; -}) => Promise; +export interface CreateAppFeatureLoader { + /** + * Returns name of this loader. suitable for showing to users. + */ + getLoaderName(): string; + + /** + * Loads a number of features dynamically. + */ + load(options: { config: ConfigApi }): Promise<{ + features: FrontendFeature[]; + }>; +} /** @public */ export function createApp(options?: { @@ -266,20 +276,20 @@ export function createApp(options?: { const discoveredFeatures = getAvailableFeatures(config); const providedFeatures: FrontendFeature[] = []; - for (const feature of options?.features ?? []) { - if (typeof feature === 'function') { + for (const entry of options?.features ?? []) { + if ('load' in entry) { try { - const loadedFeatures = await feature({ config }); - providedFeatures.push(...loadedFeatures); + const result = await entry.load({ config }); + providedFeatures.push(...result.features); } catch (e) { throw new Error( - `Failed to read frontend features from loader, ${stringifyError( + `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError( e, )}`, ); } } else { - providedFeatures.push(feature); + providedFeatures.push(entry); } }