From 19f2aa04a8e655cb762200da526e77cc6923dcac Mon Sep 17 00:00:00 2001 From: Musaab Elfaqih Date: Thu, 24 Apr 2025 14:03:09 +0200 Subject: [PATCH 1/6] Add support for filtering the extensions config by what is discovered in the provided features Signed-off-by: Musaab Elfaqih --- .../src/tree/readAppExtensionsConfig.ts | 30 +++++++++++++++++-- .../src/wiring/createSpecializedApp.tsx | 2 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts index cca7064add..e676e1ee41 100644 --- a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts +++ b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts @@ -16,6 +16,7 @@ import { Config } from '@backstage/config'; import { JsonValue } from '@backstage/types'; +import { FrontendFeature } from '../wiring'; export interface ExtensionParameters { id: string; @@ -26,12 +27,34 @@ export interface ExtensionParameters { const knownExtensionParameters = ['attachTo', 'disabled', 'config']; +/** + * Returns a filtered list of extensions based on what is discovered in the + * provided features. + */ +export const filterExtensionsByFeatures = ( + extensions: ExtensionParameters[], + features: FrontendFeature[], +): ExtensionParameters[] => { + // Get a list of all extension IDs discovered in the provided features. + const discoveredExtensionIds = features.flatMap(feature => + ('extensions' in feature && Array.isArray(feature.extensions) + ? feature.extensions + : [] + ).map((extension: { id: string }) => extension.id), + ); + + return extensions.filter(extension => + discoveredExtensionIds.includes(extension.id), + ); +}; + // Since we'll never merge arrays in config the config reader context // isn't too much of a help. Fall back to manual config reading logic // as the Config interface makes it quite hard for us otherwise. /** @internal */ export function readAppExtensionsConfig( rootConfig: Config, + features: FrontendFeature[] = [], ): ExtensionParameters[] { const arr = rootConfig.getOptional('app.extensions'); if (!Array.isArray(arr)) { @@ -43,8 +66,11 @@ export function readAppExtensionsConfig( return []; } - return arr.map((arrayEntry, arrayIndex) => - expandShorthandExtensionParameters(arrayEntry, arrayIndex), + return filterExtensionsByFeatures( + arr.map((arrayEntry, arrayIndex) => + expandShorthandExtensionParameters(arrayEntry, arrayIndex), + ), + features, ); } diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 21fde6547b..29d6b7a0b7 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -219,7 +219,7 @@ export function createSpecializedApp(options?: { builtinExtensions: [ resolveExtensionDefinition(Root, { namespace: 'root' }), ], - parameters: readAppExtensionsConfig(config), + parameters: readAppExtensionsConfig(config, features), forbidden: new Set(['root']), }), ); From 679207e918178e2a51626bbfad4f25772c45ae39 Mon Sep 17 00:00:00 2001 From: Musaab Elfaqih Date: Thu, 24 Apr 2025 17:22:08 +0200 Subject: [PATCH 2/6] Ignore stale extension config errors based on flag Signed-off-by: Musaab Elfaqih --- .../src/tree/readAppExtensionsConfig.ts | 30 ++----------------- .../src/tree/resolveAppNodeSpecs.ts | 4 ++- .../src/wiring/createSpecializedApp.tsx | 5 +++- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts index e676e1ee41..cca7064add 100644 --- a/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts +++ b/packages/frontend-app-api/src/tree/readAppExtensionsConfig.ts @@ -16,7 +16,6 @@ import { Config } from '@backstage/config'; import { JsonValue } from '@backstage/types'; -import { FrontendFeature } from '../wiring'; export interface ExtensionParameters { id: string; @@ -27,34 +26,12 @@ export interface ExtensionParameters { const knownExtensionParameters = ['attachTo', 'disabled', 'config']; -/** - * Returns a filtered list of extensions based on what is discovered in the - * provided features. - */ -export const filterExtensionsByFeatures = ( - extensions: ExtensionParameters[], - features: FrontendFeature[], -): ExtensionParameters[] => { - // Get a list of all extension IDs discovered in the provided features. - const discoveredExtensionIds = features.flatMap(feature => - ('extensions' in feature && Array.isArray(feature.extensions) - ? feature.extensions - : [] - ).map((extension: { id: string }) => extension.id), - ); - - return extensions.filter(extension => - discoveredExtensionIds.includes(extension.id), - ); -}; - // Since we'll never merge arrays in config the config reader context // isn't too much of a help. Fall back to manual config reading logic // as the Config interface makes it quite hard for us otherwise. /** @internal */ export function readAppExtensionsConfig( rootConfig: Config, - features: FrontendFeature[] = [], ): ExtensionParameters[] { const arr = rootConfig.getOptional('app.extensions'); if (!Array.isArray(arr)) { @@ -66,11 +43,8 @@ export function readAppExtensionsConfig( return []; } - return filterExtensionsByFeatures( - arr.map((arrayEntry, arrayIndex) => - expandShorthandExtensionParameters(arrayEntry, arrayIndex), - ), - features, + return arr.map((arrayEntry, arrayIndex) => + expandShorthandExtensionParameters(arrayEntry, arrayIndex), ); } diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index 0abbbb3560..cf28a80b6a 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -33,12 +33,14 @@ export function resolveAppNodeSpecs(options: { builtinExtensions?: Extension[]; parameters?: Array; forbidden?: Set; + ignoreStaleExtensionConfig?: boolean; }): AppNodeSpec[] { const { builtinExtensions = [], parameters = [], forbidden = new Set(), features = [], + ignoreStaleExtensionConfig = false, } = options; const plugins = features.filter(OpaqueFrontendPlugin.isType); @@ -202,7 +204,7 @@ export function resolveAppNodeSpecs(options: { existing.params.disabled = Boolean(overrideParam.disabled); } order.set(extensionId, existing); - } else { + } else if (!ignoreStaleExtensionConfig) { throw new Error(`Extension ${extensionId} does not exist`); } } diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 29d6b7a0b7..011773a1df 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -219,8 +219,11 @@ export function createSpecializedApp(options?: { builtinExtensions: [ resolveExtensionDefinition(Root, { namespace: 'root' }), ], - parameters: readAppExtensionsConfig(config, features), + parameters: readAppExtensionsConfig(config), forbidden: new Set(['root']), + ignoreStaleExtensionConfig: config.getOptionalBoolean( + 'app.ignoreStaleExtensionConfig', + ), }), ); From 86c5c6a72c8ed91fe7a2219084cf1c3e9f0dff26 Mon Sep 17 00:00:00 2001 From: Musaab Elfaqih Date: Thu, 24 Apr 2025 17:35:32 +0200 Subject: [PATCH 3/6] Use local flag instead of static config Signed-off-by: Musaab Elfaqih --- packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts | 6 +++--- .../frontend-app-api/src/wiring/createSpecializedApp.tsx | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index cf28a80b6a..3da68dcb50 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -33,14 +33,14 @@ export function resolveAppNodeSpecs(options: { builtinExtensions?: Extension[]; parameters?: Array; forbidden?: Set; - ignoreStaleExtensionConfig?: boolean; + allowUnknownExtensionConfig?: boolean; }): AppNodeSpec[] { const { builtinExtensions = [], parameters = [], forbidden = new Set(), features = [], - ignoreStaleExtensionConfig = false, + allowUnknownExtensionConfig = false, } = options; const plugins = features.filter(OpaqueFrontendPlugin.isType); @@ -204,7 +204,7 @@ export function resolveAppNodeSpecs(options: { existing.params.disabled = Boolean(overrideParam.disabled); } order.set(extensionId, existing); - } else if (!ignoreStaleExtensionConfig) { + } else if (!allowUnknownExtensionConfig) { throw new Error(`Extension ${extensionId} does not exist`); } } diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 011773a1df..3d356e2a74 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -208,6 +208,7 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; + flags?: Record; }): { apis: ApiHolder; tree: AppTree } { const config = options?.config ?? new ConfigReader({}, 'empty-config'); const features = deduplicateFeatures(options?.features ?? []); @@ -221,9 +222,7 @@ export function createSpecializedApp(options?: { ], parameters: readAppExtensionsConfig(config), forbidden: new Set(['root']), - ignoreStaleExtensionConfig: config.getOptionalBoolean( - 'app.ignoreStaleExtensionConfig', - ), + allowUnknownExtensionConfig: options?.flags?.allowUnknownExtensionConfig, }), ); From 1f044910d3c518ae4de90d2f75f8ff1f278a4d7c Mon Sep 17 00:00:00 2001 From: Musaab Elfaqih Date: Thu, 24 Apr 2025 17:44:53 +0200 Subject: [PATCH 4/6] Clean up + changeset Signed-off-by: Musaab Elfaqih --- .changeset/polite-shoes-allow.md | 5 +++++ packages/frontend-app-api/report.api.md | 6 ++++++ .../frontend-app-api/src/wiring/createSpecializedApp.tsx | 4 ++-- packages/frontend-app-api/src/wiring/types.ts | 5 +++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .changeset/polite-shoes-allow.md diff --git a/.changeset/polite-shoes-allow.md b/.changeset/polite-shoes-allow.md new file mode 100644 index 0000000000..d3c5e34d7a --- /dev/null +++ b/.changeset/polite-shoes-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Added the ability to ignore unknown extension config. diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index f24d662df4..661da88210 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -34,6 +34,7 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; + flags?: SpecializedAppFlags; }): { apis: ApiHolder; tree: AppTree; @@ -41,4 +42,9 @@ export function createSpecializedApp(options?: { // @public @deprecated (undocumented) export type FrontendFeature = FrontendFeature_2; + +// @public (undocumented) +export type SpecializedAppFlags = { + allowUnknownExtensionConfig?: boolean; +}; ``` diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 3d356e2a74..e1b7f56227 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -74,7 +74,7 @@ import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy'; import { BackstageRouteObject } from '../routing/types'; -import { FrontendFeature, RouteInfo } from './types'; +import { FrontendFeature, RouteInfo, SpecializedAppFlags } from './types'; import { matchRoutes } from 'react-router-dom'; function deduplicateFeatures( @@ -208,7 +208,7 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; - flags?: Record; + flags?: SpecializedAppFlags; }): { apis: ApiHolder; tree: AppTree } { const config = options?.config ?? new ConfigReader({}, 'empty-config'); const features = deduplicateFeatures(options?.features ?? []); diff --git a/packages/frontend-app-api/src/wiring/types.ts b/packages/frontend-app-api/src/wiring/types.ts index 2bf08f327f..d7af7bd528 100644 --- a/packages/frontend-app-api/src/wiring/types.ts +++ b/packages/frontend-app-api/src/wiring/types.ts @@ -28,3 +28,8 @@ export type RouteInfo = { routeParents: Map; routeObjects: BackstageRouteObject[]; }; + +/** @public */ +export type SpecializedAppFlags = { + allowUnknownExtensionConfig?: boolean; +}; From 1654d7288559b453f4eb2580e3fb83937c67b7df Mon Sep 17 00:00:00 2001 From: Musaab Elfaqih Date: Fri, 25 Apr 2025 14:52:51 +0200 Subject: [PATCH 5/6] Simplify types Signed-off-by: Musaab Elfaqih --- .changeset/polite-shoes-allow.md | 2 +- packages/frontend-app-api/report.api.md | 7 +------ .../frontend-app-api/src/wiring/createSpecializedApp.tsx | 4 ++-- packages/frontend-app-api/src/wiring/types.ts | 5 ----- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.changeset/polite-shoes-allow.md b/.changeset/polite-shoes-allow.md index d3c5e34d7a..b7e06bce6f 100644 --- a/.changeset/polite-shoes-allow.md +++ b/.changeset/polite-shoes-allow.md @@ -2,4 +2,4 @@ '@backstage/frontend-app-api': patch --- -Added the ability to ignore unknown extension config. +Added the ability to ignore unknown extension config by passing `{ flags: { allowUnknownExtensionConfig: true } }` to `createSpecializedApp`. diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 661da88210..91a33d277d 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -34,7 +34,7 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; - flags?: SpecializedAppFlags; + flags?: Record; }): { apis: ApiHolder; tree: AppTree; @@ -42,9 +42,4 @@ export function createSpecializedApp(options?: { // @public @deprecated (undocumented) export type FrontendFeature = FrontendFeature_2; - -// @public (undocumented) -export type SpecializedAppFlags = { - allowUnknownExtensionConfig?: boolean; -}; ``` diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index e1b7f56227..3d356e2a74 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -74,7 +74,7 @@ import { ApiRegistry } from '../../../core-app-api/src/apis/system/ApiRegistry'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy'; import { BackstageRouteObject } from '../routing/types'; -import { FrontendFeature, RouteInfo, SpecializedAppFlags } from './types'; +import { FrontendFeature, RouteInfo } from './types'; import { matchRoutes } from 'react-router-dom'; function deduplicateFeatures( @@ -208,7 +208,7 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; - flags?: SpecializedAppFlags; + flags?: Record; }): { apis: ApiHolder; tree: AppTree } { const config = options?.config ?? new ConfigReader({}, 'empty-config'); const features = deduplicateFeatures(options?.features ?? []); diff --git a/packages/frontend-app-api/src/wiring/types.ts b/packages/frontend-app-api/src/wiring/types.ts index d7af7bd528..2bf08f327f 100644 --- a/packages/frontend-app-api/src/wiring/types.ts +++ b/packages/frontend-app-api/src/wiring/types.ts @@ -28,8 +28,3 @@ export type RouteInfo = { routeParents: Map; routeObjects: BackstageRouteObject[]; }; - -/** @public */ -export type SpecializedAppFlags = { - allowUnknownExtensionConfig?: boolean; -}; From ab2180d85563d7e7294c9e007912711a41bed292 Mon Sep 17 00:00:00 2001 From: Musaab Elfaqih Date: Mon, 12 May 2025 12:18:37 +0200 Subject: [PATCH 6/6] use more specific flags type Signed-off-by: Musaab Elfaqih --- packages/frontend-app-api/report.api.md | 4 +++- packages/frontend-app-api/src/wiring/createSpecializedApp.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/frontend-app-api/report.api.md b/packages/frontend-app-api/report.api.md index 91a33d277d..d2a3ac4d3b 100644 --- a/packages/frontend-app-api/report.api.md +++ b/packages/frontend-app-api/report.api.md @@ -34,7 +34,9 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; - flags?: Record; + flags?: { + allowUnknownExtensionConfig?: boolean; + }; }): { apis: ApiHolder; tree: AppTree; diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx index 3d356e2a74..dbe5868176 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.tsx @@ -208,7 +208,7 @@ export function createSpecializedApp(options?: { extensionFactoryMiddleware?: | ExtensionFactoryMiddleware | ExtensionFactoryMiddleware[]; - flags?: Record; + flags?: { allowUnknownExtensionConfig?: boolean }; }): { apis: ApiHolder; tree: AppTree } { const config = options?.config ?? new ConfigReader({}, 'empty-config'); const features = deduplicateFeatures(options?.features ?? []);