@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Removed `featureLoader` from `createApp`, `features` instead accepts both `FrontendFeature` and `CreateAppFeatureLoader`
|
||||
@@ -14,16 +14,20 @@ 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 type CreateAppFeatureLoader = (options: {
|
||||
config: ConfigApi;
|
||||
}) => Promise<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:^",
|
||||
|
||||
@@ -99,6 +99,33 @@ describe('createApp', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should support feature loaders', async () => {
|
||||
const app = createApp({
|
||||
configLoader: async () => ({
|
||||
config: new MockConfigApi({ key: 'config-value' }),
|
||||
}),
|
||||
features: [
|
||||
async ({ config }) => [
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
createPageExtension({
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>{config.getString('key')}</div>,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
await renderWithEffects(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByText('config-value'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
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,20 @@ function deduplicateFeatures(
|
||||
.reverse();
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of dynamically loaded frontend features.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type CreateAppFeatureLoader = (options: {
|
||||
config: ConfigApi;
|
||||
}) => Promise<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 +264,28 @@ export function createApp(options?: {
|
||||
);
|
||||
|
||||
const discoveredFeatures = getAvailableFeatures(config);
|
||||
const loadedFeatures = (await options?.featureLoader?.({ config })) ?? [];
|
||||
|
||||
const providedFeatures: FrontendFeature[] = [];
|
||||
for (const feature of options?.features ?? []) {
|
||||
if (typeof feature === 'function') {
|
||||
try {
|
||||
const loadedFeatures = await feature({ config });
|
||||
providedFeatures.push(...loadedFeatures);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to read frontend features from loader, ${stringifyError(
|
||||
e,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
providedFeatures.push(feature);
|
||||
}
|
||||
}
|
||||
|
||||
const app = createSpecializedApp({
|
||||
config,
|
||||
features: [
|
||||
...discoveredFeatures,
|
||||
...loadedFeatures,
|
||||
...(options?.features ?? []),
|
||||
],
|
||||
features: [...discoveredFeatures, ...providedFeatures],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
}).createRoot();
|
||||
|
||||
@@ -285,6 +307,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