Keep feature discovery and resolution logic separate

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2025-03-10 09:51:47 +01:00
parent f008f26970
commit 55e8568ff5
6 changed files with 62 additions and 37 deletions
+1 -1
View File
@@ -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.
+3
View File
@@ -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;
+10 -5
View File
@@ -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,
});
+1 -30
View File
@@ -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<FrontendFeature[]> {
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;
}
+2 -1
View File
@@ -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';
@@ -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<FrontendFeature[]> {
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;
}