From 76832a9f050b4a3696195c1bf5c25e6796c3a112 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 31 Jul 2025 13:35:38 +0200 Subject: [PATCH] frontend-defaults: removed deprecated CreateAppFeatureLoader Signed-off-by: Patrik Oldsberg --- .changeset/fruity-rockets-rhyme.md | 5 ++ packages/frontend-defaults/report.api.md | 20 +----- .../frontend-defaults/src/createApp.test.tsx | 49 +++++-------- packages/frontend-defaults/src/createApp.tsx | 26 +------ packages/frontend-defaults/src/index.ts | 6 +- .../frontend-defaults/src/resolution.test.ts | 70 ------------------- packages/frontend-defaults/src/resolution.ts | 30 +------- 7 files changed, 30 insertions(+), 176 deletions(-) create mode 100644 .changeset/fruity-rockets-rhyme.md diff --git a/.changeset/fruity-rockets-rhyme.md b/.changeset/fruity-rockets-rhyme.md new file mode 100644 index 0000000000..13e957c5f1 --- /dev/null +++ b/.changeset/fruity-rockets-rhyme.md @@ -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. diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md index a5fe3e5f07..37c770074e 100644 --- a/packages/frontend-defaults/report.api.md +++ b/packages/frontend-defaults/report.api.md @@ -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[]; }>; diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 1d96c22ad0..5bf8356c08 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -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 () =>
{config.getString('key')}
, - }, - }), - ], + const loader = createFrontendFeatureLoader({ + async *loader({ config }) { + yield createFrontendPlugin({ + pluginId: 'test', + extensions: [ + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
{config.getString('key')}
, + }, }), ], - }; + }); }, - }; + }); 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/, ); }); diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index ab0ddf85a7..e6702a4fcc 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -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; /** diff --git a/packages/frontend-defaults/src/index.ts b/packages/frontend-defaults/src/index.ts index 573cc9b08c..68d4c72568 100644 --- a/packages/frontend-defaults/src/index.ts +++ b/packages/frontend-defaults/src/index.ts @@ -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'; diff --git a/packages/frontend-defaults/src/resolution.test.ts b/packages/frontend-defaults/src/resolution.test.ts index b39e302b24..991cb2d827 100644 --- a/packages/frontend-defaults/src/resolution.test.ts +++ b/packages/frontend-defaults/src/resolution.test.ts @@ -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: _ }) { diff --git a/packages/frontend-defaults/src/resolution.ts b/packages/frontend-defaults/src/resolution.ts index 6ab26c3d1d..58f91c45be 100644 --- a/packages/frontend-defaults/src/resolution.ts +++ b/packages/frontend-defaults/src/resolution.ts @@ -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 }; }