Merge pull request #21861 from backstage/freben/load

no more featureLoader
This commit is contained in:
Fredrik Adelöw
2023-12-14 16:25:07 +01:00
committed by GitHub
7 changed files with 119 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': minor
---
Removed `featureLoader` from `createApp`, `features` instead accepts both `FrontendFeature` and `CreateAppFeatureLoader`
+9 -2
View File
@@ -14,16 +14,23 @@ import { SubRouteRef } from '@backstage/frontend-plugin-api';
// @public (undocumented)
export function createApp(options?: {
features?: FrontendFeature[];
features?: (FrontendFeature | CreateAppFeatureLoader)[];
configLoader?: () => Promise<{
config: ConfigApi;
}>;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
featureLoader?: (ctx: { config: ConfigApi }) => Promise<FrontendFeature[]>;
}): {
createRoot(): JSX_2.Element;
};
// @public
export interface CreateAppFeatureLoader {
getLoaderName(): string;
load(options: { config: ConfigApi }): Promise<{
features: FrontendFeature[];
}>;
}
// @public
export type CreateAppRouteBinder = <
TExternalRoutes extends {
+1
View File
@@ -38,6 +38,7 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
@@ -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';
@@ -99,6 +99,66 @@ 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 () => <div>{config.getString('key')}</div>,
}),
],
}),
],
};
},
};
const app = createApp({
configLoader: async () => ({
config: new MockConfigApi({ key: 'config-value' }),
}),
features: [loader],
});
await renderWithEffects(app.createRoot());
await expect(
screen.findByText('config-value'),
).resolves.toBeInTheDocument();
});
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: [loader],
});
await expect(
renderWithEffects(app.createRoot()),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to read frontend features from loader 'test-loader', TypeError: boom"`,
);
});
it('should register feature flags', async () => {
const app = createApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
@@ -101,6 +101,7 @@ import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiri
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
import { stringifyError } from '@backstage/errors';
export const builtinExtensions = [
Core,
@@ -238,12 +239,30 @@ function deduplicateFeatures(
.reverse();
}
/**
* A source of dynamically loaded frontend features.
*
* @public
*/
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?: {
features?: FrontendFeature[];
features?: (FrontendFeature | CreateAppFeatureLoader)[];
configLoader?: () => Promise<{ config: ConfigApi }>;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
featureLoader?: (ctx: { config: ConfigApi }) => Promise<FrontendFeature[]>;
}): {
createRoot(): JSX.Element;
} {
@@ -255,15 +274,28 @@ export function createApp(options?: {
);
const discoveredFeatures = getAvailableFeatures(config);
const loadedFeatures = (await options?.featureLoader?.({ config })) ?? [];
const providedFeatures: FrontendFeature[] = [];
for (const entry of options?.features ?? []) {
if ('load' in entry) {
try {
const result = await entry.load({ config });
providedFeatures.push(...result.features);
} catch (e) {
throw new Error(
`Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError(
e,
)}`,
);
}
} else {
providedFeatures.push(entry);
}
}
const app = createSpecializedApp({
config,
features: [
...discoveredFeatures,
...loadedFeatures,
...(options?.features ?? []),
],
features: [...discoveredFeatures, ...providedFeatures],
bindRoutes: options?.bindRoutes,
}).createRoot();
@@ -285,6 +317,7 @@ export function createApp(options?: {
/**
* Synchronous version of {@link createApp}, expecting all features and
* config to have been loaded already.
*
* @public
*/
export function createSpecializedApp(options?: {
@@ -18,6 +18,7 @@ export {
createApp,
createSpecializedApp,
createExtensionTree,
type CreateAppFeatureLoader,
type ExtensionTreeNode,
type ExtensionTree,
} from './createApp';
+1
View File
@@ -4158,6 +4158,7 @@ __metadata:
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"