Merge pull request #21861 from backstage/freben/load
no more featureLoader
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Removed `featureLoader` from `createApp`, `features` instead accepts both `FrontendFeature` and `CreateAppFeatureLoader`
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user