From 55e8568ff54e50ba3c3169500fecd212a0693700 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 10 Mar 2025 09:51:47 +0100 Subject: [PATCH] Keep feature discovery and resolution logic separate Signed-off-by: Eric Peterson --- .changeset/mira-color-like.md | 2 +- packages/frontend-defaults/report.api.md | 3 ++ packages/frontend-defaults/src/createApp.tsx | 15 ++++--- packages/frontend-defaults/src/discovery.ts | 31 +------------ packages/frontend-defaults/src/index.ts | 3 +- .../frontend-defaults/src/resolveFeatures.ts | 45 +++++++++++++++++++ 6 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 packages/frontend-defaults/src/resolveFeatures.ts diff --git a/.changeset/mira-color-like.md b/.changeset/mira-color-like.md index 45bff186f1..7bdf035407 100644 --- a/.changeset/mira-color-like.md +++ b/.changeset/mira-color-like.md @@ -2,4 +2,4 @@ '@backstage/frontend-defaults': patch --- -Feature resolution logic used in `createApp` is now exposed via the `resolveFeatures` function. +Feature discovery and resolution logic used in `createApp` is now exposed via the `getAvailableFeatures` and `resolveFeatures` functions respectively. diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md index ac9c7a7312..a65d1a5d1f 100644 --- a/packages/frontend-defaults/report.api.md +++ b/packages/frontend-defaults/report.api.md @@ -45,6 +45,9 @@ export function createPublicSignInApp(options?: CreateAppOptions): { createRoot(): React_2.JSX.Element; }; +// @public (undocumented) +export function getAvailableFeatures(config: Config): FrontendFeature[]; + // @public (undocumented) export function resolveFeatures(options: { config: Config; diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index ae524e93e2..70c308518e 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -20,7 +20,6 @@ import { ConfigApi, coreExtensionData } from '@backstage/frontend-plugin-api'; import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs'; -import { resolveFeatures } from './discovery'; import { ConfigReader } from '@backstage/config'; import { CreateAppRouteBinder, @@ -28,6 +27,9 @@ import { FrontendFeature, createSpecializedApp, } from '@backstage/frontend-app-api'; +import appPlugin from '@backstage/plugin-app'; +import { getAvailableFeatures } from './discovery'; +import { resolveFeatures } from './resolveFeatures'; /** * A source of dynamically loaded frontend features. @@ -87,12 +89,15 @@ export function createApp(options?: CreateAppOptions): { overrideBaseUrlConfigs(defaultConfigLoaderSync()), ); + const discoveredFeatures = getAvailableFeatures(config); + const providedFeatures = await resolveFeatures({ + config, + features: options?.features, + }); + const app = createSpecializedApp({ config, - features: await resolveFeatures({ - config, - features: options?.features, - }), + features: [appPlugin, ...discoveredFeatures, ...providedFeatures], bindRoutes: options?.bindRoutes, extensionFactoryMiddleware: options?.extensionFactoryMiddleware, }); diff --git a/packages/frontend-defaults/src/discovery.ts b/packages/frontend-defaults/src/discovery.ts index c71d7d15ea..6f9d2e2d16 100644 --- a/packages/frontend-defaults/src/discovery.ts +++ b/packages/frontend-defaults/src/discovery.ts @@ -15,10 +15,7 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { stringifyError } from '@backstage/errors'; import { FrontendFeature } from '@backstage/frontend-app-api'; -import { CreateAppFeatureLoader } from './createApp'; -import appPlugin from '@backstage/plugin-app'; interface DiscoveryGlobal { modules: Array<{ name: string; export?: string; default: unknown }>; @@ -56,7 +53,7 @@ function readPackageDetectionConfig(config: Config) { } /** - * @internal + * @public */ export function getAvailableFeatures(config: Config): FrontendFeature[] { const discovered = ( @@ -97,29 +94,3 @@ function isBackstageFeature(obj: unknown): obj is FrontendFeature { } return false; } - -/** @public */ -export async function resolveFeatures(options: { - config: Config; - features?: (FrontendFeature | CreateAppFeatureLoader)[]; -}): Promise { - const discoveredFeatures = getAvailableFeatures(options.config); - const features = [appPlugin, ...discoveredFeatures]; - for (const entry of options.features ?? []) { - if ('load' in entry) { - try { - const result = await entry.load({ config: options.config }); - features.push(...result.features); - } catch (e) { - throw new Error( - `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError( - e, - )}`, - ); - } - } else { - features.push(entry); - } - } - return features; -} diff --git a/packages/frontend-defaults/src/index.ts b/packages/frontend-defaults/src/index.ts index ff233e00b3..c65e053dd5 100644 --- a/packages/frontend-defaults/src/index.ts +++ b/packages/frontend-defaults/src/index.ts @@ -26,4 +26,5 @@ export { type CreateAppFeatureLoader, } from './createApp'; export { createPublicSignInApp } from './createPublicSignInApp'; -export { resolveFeatures } from './discovery'; +export { getAvailableFeatures } from './discovery'; +export { resolveFeatures } from './resolveFeatures'; diff --git a/packages/frontend-defaults/src/resolveFeatures.ts b/packages/frontend-defaults/src/resolveFeatures.ts new file mode 100644 index 0000000000..dddf0d2a1b --- /dev/null +++ b/packages/frontend-defaults/src/resolveFeatures.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { stringifyError } from '@backstage/errors'; +import { FrontendFeature } from '@backstage/frontend-app-api'; +import { CreateAppFeatureLoader } from './createApp'; + +/** @public */ +export async function resolveFeatures(options: { + config: Config; + features?: (FrontendFeature | CreateAppFeatureLoader)[]; +}): Promise { + const features = []; + for (const entry of options.features ?? []) { + if ('load' in entry) { + try { + const result = await entry.load({ config: options.config }); + features.push(...result.features); + } catch (e) { + throw new Error( + `Failed to read frontend features from loader '${entry.getLoaderName()}', ${stringifyError( + e, + )}`, + ); + } + } else { + features.push(entry); + } + } + return features; +}