diff --git a/.changeset/rude-tomatoes-itch.md b/.changeset/rude-tomatoes-itch.md new file mode 100644 index 0000000000..1071f65756 --- /dev/null +++ b/.changeset/rude-tomatoes-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Installed features are now deduplicated both by reference and ID when available. Features passed to `createApp` now override both discovered and loaded features. diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index e030fecb5a..785e562624 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -104,6 +104,5 @@ export function collectLegacyRoutes( }, ); - // TODO: For every legacy plugin that we find, make sure any matching plugin is disabled in the new system return results; } diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 87b2042d34..8b411ac38b 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -267,6 +267,29 @@ export function createInstances(options: { return { coreInstance, instances }; } +function deduplicateFeatures( + allFeatures: (BackstagePlugin | ExtensionOverrides)[], +): (BackstagePlugin | ExtensionOverrides)[] { + // Start by removing duplicates by reference + const features = Array.from(new Set(allFeatures)); + + // Plugins are deduplicated by ID, last one wins + const seenIds = new Set(); + return features + .reverse() + .filter(feature => { + if (feature.$$type !== '@backstage/BackstagePlugin') { + return true; + } + if (seenIds.has(feature.id)) { + return false; + } + seenIds.add(feature.id); + return true; + }) + .reverse(); +} + /** @public */ export function createApp(options: { features?: (BackstagePlugin | ExtensionOverrides)[]; @@ -287,13 +310,11 @@ export function createApp(options: { const discoveredFeatures = getAvailableFeatures(config); const loadedFeatures = (await options.featureLoader?.({ config })) ?? []; - const allFeatures = Array.from( - new Set([ - ...discoveredFeatures, - ...(options.features ?? []), - ...loadedFeatures, - ]), - ); + const allFeatures = deduplicateFeatures([ + ...discoveredFeatures, + ...loadedFeatures, + ...(options.features ?? []), + ]); const { coreInstance } = createInstances({ features: allFeatures,