frontend-defaults: removed deprecated CreateAppFeatureLoader

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-07-31 13:35:38 +02:00
parent 484e500f49
commit 76832a9f05
7 changed files with 30 additions and 176 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-defaults': minor
---
**BREAKING**: Removed the deprecated `CreateAppFeatureLoader` and support for it in other APIs. Switch existing usage to use the newer `createFrontendFeatureLoader` from `@backstage/frontend-plugin-api` instead.
+2 -18
View File
@@ -18,14 +18,6 @@ export function createApp(options?: CreateAppOptions): {
createRoot(): JSX_2.Element;
};
// @public @deprecated
export interface CreateAppFeatureLoader {
getLoaderName(): string;
load(options: { config: ConfigApi }): Promise<{
features: FrontendFeature[];
}>;
}
// @public
export interface CreateAppOptions {
// (undocumented)
@@ -39,11 +31,7 @@ export interface CreateAppOptions {
| ExtensionFactoryMiddleware
| ExtensionFactoryMiddleware[];
// (undocumented)
features?: (
| FrontendFeature
| FrontendFeatureLoader
| CreateAppFeatureLoader
)[];
features?: (FrontendFeature | FrontendFeatureLoader)[];
// (undocumented)
flags?: {
allowUnknownExtensionConfig?: boolean;
@@ -66,11 +54,7 @@ export function discoverAvailableFeatures(config: Config): {
// @public (undocumented)
export function resolveAsyncFeatures(options: {
config: Config;
features?: (
| FrontendFeature
| FrontendFeatureLoader
| CreateAppFeatureLoader
)[];
features?: (FrontendFeature | FrontendFeatureLoader)[];
}): Promise<{
features: FrontendFeature[];
}>;
@@ -21,13 +21,14 @@ import {
createExtension,
PageBlueprint,
createFrontendPlugin,
createFrontendFeatureLoader,
ThemeBlueprint,
createFrontendModule,
useAppNode,
FrontendPluginInfo,
} from '@backstage/frontend-plugin-api';
import { screen, waitFor } from '@testing-library/react';
import { CreateAppFeatureLoader, createApp } from './createApp';
import { createApp } from './createApp';
import { mockApis, renderWithEffects } from '@backstage/test-utils';
import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api';
import { default as appPluginOriginal } from '@backstage/plugin-app';
@@ -169,28 +170,21 @@ describe('createApp', () => {
});
it('should support feature loaders', async () => {
const loader: CreateAppFeatureLoader = {
getLoaderName() {
return 'test-loader';
},
async load({ config }) {
return {
features: [
createFrontendPlugin({
pluginId: 'test',
extensions: [
PageBlueprint.make({
params: {
defaultPath: '/',
loader: async () => <div>{config.getString('key')}</div>,
},
}),
],
const loader = createFrontendFeatureLoader({
async *loader({ config }) {
yield createFrontendPlugin({
pluginId: 'test',
extensions: [
PageBlueprint.make({
params: {
defaultPath: '/',
loader: async () => <div>{config.getString('key')}</div>,
},
}),
],
};
});
},
};
});
const app = createApp({
configLoader: async () => ({
@@ -207,14 +201,11 @@ describe('createApp', () => {
});
it('should propagate errors thrown by feature loaders', async () => {
const loader: CreateAppFeatureLoader = {
getLoaderName() {
return 'test-loader';
},
async load() {
const loader = createFrontendFeatureLoader({
async loader() {
throw new TypeError('boom');
},
};
});
const app = createApp({
configLoader: async () => ({
@@ -223,10 +214,8 @@ describe('createApp', () => {
features: [loader],
});
await expect(
renderWithEffects(app.createRoot()),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to read frontend features from loader 'test-loader', TypeError: boom"`,
await expect(renderWithEffects(app.createRoot())).rejects.toThrow(
/Failed to read frontend features from loader created at '.*\/createApp\.test\.tsx:\d+:\d+': TypeError: boom/,
);
});
+1 -25
View File
@@ -36,37 +36,13 @@ import appPlugin from '@backstage/plugin-app';
import { discoverAvailableFeatures } from './discovery';
import { resolveAsyncFeatures } from './resolution';
/**
* A source of dynamically loaded frontend features.
*
* @public
* @deprecated Use the {@link @backstage/frontend-plugin-api#createFrontendFeatureLoader} function instead.
*/
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[];
}>;
}
/**
* Options for {@link createApp}.
*
* @public
*/
export interface CreateAppOptions {
features?: (
| FrontendFeature
| FrontendFeatureLoader
| CreateAppFeatureLoader
)[];
features?: (FrontendFeature | FrontendFeatureLoader)[];
configLoader?: () => Promise<{ config: ConfigApi }>;
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
/**
+1 -5
View File
@@ -20,11 +20,7 @@
* @packageDocumentation
*/
export {
createApp,
type CreateAppOptions,
type CreateAppFeatureLoader,
} from './createApp';
export { createApp, type CreateAppOptions } from './createApp';
export { createPublicSignInApp } from './createPublicSignInApp';
export { discoverAvailableFeatures } from './discovery';
export { resolveAsyncFeatures } from './resolution';
@@ -20,7 +20,6 @@ import {
FrontendFeatureLoader,
PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { CreateAppFeatureLoader } from './createApp';
import { resolveAsyncFeatures } from './resolution';
import { mockApis } from '@backstage/test-utils';
@@ -71,75 +70,6 @@ describe('resolveAsyncFeatures', () => {
]);
});
it('supports deprecated feature loaders', async () => {
const loader: CreateAppFeatureLoader = {
getLoaderName() {
return 'test-loader';
},
async load() {
return {
features: [
createFrontendPlugin({
pluginId: 'test',
extensions: [
PageBlueprint.make({
params: {
defaultPath: '/',
loader: () => new Promise(() => {}),
},
}),
],
}),
],
};
},
};
const { features } = await resolveAsyncFeatures({
config: mockApis.config(),
features: [loader],
});
expect(features).toMatchObject([
{
$$type: '@backstage/FrontendPlugin',
id: 'test',
version: 'v1',
extensions: [
{
$$type: '@backstage/Extension',
id: 'page:test',
version: 'v2',
attachTo: {
id: 'app/routes',
input: 'routes',
},
},
],
},
]);
});
it('should propagate errors thrown by deprecated feature loaders', async () => {
const loader: CreateAppFeatureLoader = {
getLoaderName() {
return 'test-loader';
},
async load() {
throw new TypeError('boom');
},
};
await expect(() =>
resolveAsyncFeatures({
config: mockApis.config(),
features: [loader],
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to read frontend features from loader 'test-loader', TypeError: boom"`,
);
});
it('supports feature loaders', async () => {
const loader: FrontendFeatureLoader = createFrontendFeatureLoader({
async loader({ config: _ }) {
+2 -28
View File
@@ -20,40 +20,14 @@ import {
FrontendFeature,
FrontendFeatureLoader,
} from '@backstage/frontend-plugin-api';
import { CreateAppFeatureLoader } from './createApp';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { isInternalFrontendFeatureLoader } from '../../frontend-plugin-api/src/wiring/createFrontendFeatureLoader';
/** @public */
export async function resolveAsyncFeatures(options: {
config: Config;
features?: (
| FrontendFeature
| FrontendFeatureLoader
| CreateAppFeatureLoader
)[];
features?: (FrontendFeature | FrontendFeatureLoader)[];
}): Promise<{ features: FrontendFeature[] }> {
const features: (FrontendFeature | FrontendFeatureLoader)[] = [];
// Separate deprecated CreateAppFeatureLoader elements from the frontend features,
// and manage the deprecated elements first.
for (const item of options?.features ?? []) {
if ('load' in item) {
try {
const result = await item.load({ config: options.config });
features.push(...result.features);
} catch (e) {
throw new Error(
`Failed to read frontend features from loader '${item.getLoaderName()}', ${stringifyError(
e,
)}`,
);
}
} else {
features.push(item);
}
}
const loadedFeatures: FrontendFeature[] = [];
const alreadyMetFeatureLoaders: FrontendFeatureLoader[] = [];
const maxRecursionDepth = 5;
@@ -96,7 +70,7 @@ export async function resolveAsyncFeatures(options: {
}
}
await applyFeatureLoaders(features, 1);
await applyFeatureLoaders(options.features ?? [], 1);
return { features: loadedFeatures };
}