From 8e21c4d9931759ba188ab9abb6c27a411ad373d1 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 7 Aug 2025 12:29:58 +0200 Subject: [PATCH 1/2] refactor(frontend-plugin,app-api): make app spec plugin required Signed-off-by: Camila Belo --- .changeset/fluffy-otters-cry.md | 5 +++ .changeset/thirty-eagles-run.md | 5 +++ .../src/tree/instantiateAppNodeTree.test.ts | 3 +- .../src/tree/resolveAppNodeSpecs.test.ts | 43 ++++++++++++++++++- .../src/tree/resolveAppNodeSpecs.ts | 12 ++++-- .../src/tree/resolveAppTree.test.ts | 42 +++++++++++++++--- .../src/apis/definitions/AppTreeApi.ts | 2 +- 7 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 .changeset/fluffy-otters-cry.md create mode 100644 .changeset/thirty-eagles-run.md diff --git a/.changeset/fluffy-otters-cry.md b/.changeset/fluffy-otters-cry.md new file mode 100644 index 0000000000..2798473c2f --- /dev/null +++ b/.changeset/fluffy-otters-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +Use an empty root plugin for built-in extension app node specs. diff --git a/.changeset/thirty-eagles-run.md b/.changeset/thirty-eagles-run.md new file mode 100644 index 0000000000..af2809e3f4 --- /dev/null +++ b/.changeset/thirty-eagles-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': minor +--- + +**BREAKING:** The `AppNodeSpec.plugin` property is now required. diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index f3201e97b2..794a2a3671 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -24,6 +24,7 @@ import { createExtension, createExtensionDataRef, createExtensionInput, + createFrontendPlugin, } from '@backstage/frontend-plugin-api'; import { createAppNodeInstance, @@ -56,7 +57,7 @@ function makeSpec( attachTo: extension.attachTo, disabled: extension.disabled, extension: extension as Extension, - plugin: undefined, + plugin: createFrontendPlugin({ pluginId: 'root' }), ...spec, }; } diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 979c851a47..276f47c9d8 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -20,7 +20,7 @@ import { Extension, ExtensionDefinition, } from '@backstage/frontend-plugin-api'; -import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; +import { resolveAppNodeSpecs, rootPlugin } from './resolveAppNodeSpecs'; function makeExt( id: string, @@ -62,12 +62,15 @@ describe('resolveAppNodeSpecs', () => { builtinExtensions: [a], parameters: [], }), - ).toEqual([ + ).toStrictEqual([ { id: 'a', extension: a, attachTo: { id: 'root', input: 'default' }, disabled: true, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, ]); }); @@ -87,12 +90,18 @@ describe('resolveAppNodeSpecs', () => { extension: a, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'b', extension: b, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, ]); }); @@ -120,6 +129,9 @@ describe('resolveAppNodeSpecs', () => { extension: b, attachTo: { id: 'derp', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'test/a', @@ -204,12 +216,18 @@ describe('resolveAppNodeSpecs', () => { extension: b, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'a', extension: a, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, ]); }); @@ -238,42 +256,63 @@ describe('resolveAppNodeSpecs', () => { extension: e, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'd', extension: d, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'c', extension: c, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'a', extension: a, attachTo: { id: 'root', input: 'default' }, disabled: true, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'b', extension: b, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'f', extension: f, attachTo: { id: 'root', input: 'default' }, disabled: false, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, { id: 'g', extension: g, attachTo: { id: 'root', input: 'default' }, disabled: true, + config: undefined, + plugin: rootPlugin, + source: rootPlugin, }, ]); }); diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index 4788734800..46a6042729 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Extension, FrontendFeature } from '@backstage/frontend-plugin-api'; +import { + createFrontendPlugin, + Extension, + FrontendFeature, +} from '@backstage/frontend-plugin-api'; import { ExtensionParameters } from './readAppExtensionsConfig'; import { AppNodeSpec } from '@backstage/frontend-plugin-api'; import { OpaqueFrontendPlugin } from '@internal/frontend'; @@ -26,6 +30,8 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +export const rootPlugin = createFrontendPlugin({ pluginId: 'root' }); + /** @internal */ export function resolveAppNodeSpecs(options: { features?: FrontendFeature[]; @@ -106,8 +112,8 @@ export function resolveAppNodeSpecs(options: { return { extension: internalExtension, params: { - source: undefined, - plugin: undefined, + source: rootPlugin, + plugin: rootPlugin, attachTo: internalExtension.attachTo, disabled: internalExtension.disabled, config: undefined as unknown, diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index e93d311f8d..3efd500a78 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -18,6 +18,7 @@ import { coreExtensionData, createExtension, createExtensionInput, + createFrontendPlugin, Extension, } from '@backstage/frontend-plugin-api'; import { resolveAppTree } from './resolveAppTree'; @@ -37,6 +38,7 @@ const baseSpec = { extension, attachTo: { id: 'nonexistent', input: 'nonexistent' }, disabled: false, + plugin: createFrontendPlugin({ pluginId: 'root' }), }; describe('buildAppTree', () => { @@ -284,8 +286,20 @@ describe('buildAppTree', () => { ) as Extension; const tree = resolveAppTree('a', [ - { attachTo: e1.attachTo, id: 'a', extension: e1, disabled: false }, - { attachTo: e2.attachTo, id: 'b', extension: e2, disabled: false }, + { + attachTo: e1.attachTo, + id: 'a', + extension: e1, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e2.attachTo, + id: 'b', + extension: e2, + disabled: false, + plugin: baseSpec.plugin, + }, ]); expect(tree.root).toMatchInlineSnapshot(` @@ -352,9 +366,27 @@ describe('buildAppTree', () => { ) as Extension; const tree = resolveAppTree('test-2', [ - { attachTo: e1.attachTo, id: e1.id, extension: e1, disabled: false }, - { attachTo: e2.attachTo, id: e2.id, extension: e2, disabled: false }, - { attachTo: e3.attachTo, id: e3.id, extension: e3, disabled: false }, + { + attachTo: e1.attachTo, + id: e1.id, + extension: e1, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e2.attachTo, + id: e2.id, + extension: e2, + disabled: false, + plugin: baseSpec.plugin, + }, + { + attachTo: e3.attachTo, + id: e3.id, + extension: e3, + disabled: false, + plugin: baseSpec.plugin, + }, ]); expect(tree.nodes.get('test-3')?.edges.attachedTo?.node).toBe( diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts index c205790aae..1a4752a81f 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts @@ -37,7 +37,7 @@ export interface AppNodeSpec { readonly extension: Extension; readonly disabled: boolean; readonly config?: unknown; - readonly plugin?: FrontendPlugin; + readonly plugin: FrontendPlugin; } /** From 18509cea1d27506bface247121dccb37cfbc694d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 8 Aug 2025 10:20:38 +0200 Subject: [PATCH 2/2] refactor: apply review suggestions Signed-off-by: Camila Belo --- .changeset/fluffy-otters-cry.md | 2 +- .changeset/thirty-eagles-run.md | 2 +- .../src/tree/instantiateAppNodeTree.test.ts | 2 +- .../src/tree/resolveAppNodeSpecs.test.ts | 56 +++++++++---------- .../src/tree/resolveAppNodeSpecs.ts | 12 ++-- .../src/tree/resolveAppTree.test.ts | 2 +- packages/frontend-plugin-api/report.api.md | 2 +- 7 files changed, 41 insertions(+), 37 deletions(-) diff --git a/.changeset/fluffy-otters-cry.md b/.changeset/fluffy-otters-cry.md index 2798473c2f..fd24a0bb47 100644 --- a/.changeset/fluffy-otters-cry.md +++ b/.changeset/fluffy-otters-cry.md @@ -2,4 +2,4 @@ '@backstage/frontend-app-api': minor --- -Use an empty root plugin for built-in extension app node specs. +Use an app plugin for built-in extension app node specs. diff --git a/.changeset/thirty-eagles-run.md b/.changeset/thirty-eagles-run.md index af2809e3f4..10ca1af381 100644 --- a/.changeset/thirty-eagles-run.md +++ b/.changeset/thirty-eagles-run.md @@ -2,4 +2,4 @@ '@backstage/frontend-app-api': minor --- -**BREAKING:** The `AppNodeSpec.plugin` property is now required. +The `AppNodeSpec.plugin` property is now required. diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 794a2a3671..ebe8d95263 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -57,7 +57,7 @@ function makeSpec( attachTo: extension.attachTo, disabled: extension.disabled, extension: extension as Extension, - plugin: createFrontendPlugin({ pluginId: 'root' }), + plugin: createFrontendPlugin({ pluginId: 'app' }), ...spec, }; } diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 276f47c9d8..83abcf3dc4 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -20,7 +20,7 @@ import { Extension, ExtensionDefinition, } from '@backstage/frontend-plugin-api'; -import { resolveAppNodeSpecs, rootPlugin } from './resolveAppNodeSpecs'; +import { resolveAppNodeSpecs } from './resolveAppNodeSpecs'; function makeExt( id: string, @@ -62,15 +62,15 @@ describe('resolveAppNodeSpecs', () => { builtinExtensions: [a], parameters: [], }), - ).toStrictEqual([ + ).toEqual([ { id: 'a', extension: a, attachTo: { id: 'root', input: 'default' }, disabled: true, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, ]); }); @@ -91,8 +91,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'b', @@ -100,8 +100,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, ]); }); @@ -130,8 +130,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'derp', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'test/a', @@ -217,8 +217,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'a', @@ -226,8 +226,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, ]); }); @@ -257,8 +257,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'd', @@ -266,8 +266,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'c', @@ -275,8 +275,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'a', @@ -284,8 +284,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: true, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'b', @@ -293,8 +293,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'f', @@ -302,8 +302,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: false, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, { id: 'g', @@ -311,8 +311,8 @@ describe('resolveAppNodeSpecs', () => { attachTo: { id: 'root', input: 'default' }, disabled: true, config: undefined, - plugin: rootPlugin, - source: rootPlugin, + plugin: expect.any(Object), + source: expect.any(Object), }, ]); }); diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts index 46a6042729..f9ce7387c2 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.ts @@ -30,8 +30,6 @@ import { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtension } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; -export const rootPlugin = createFrontendPlugin({ pluginId: 'root' }); - /** @internal */ export function resolveAppNodeSpecs(options: { features?: FrontendFeature[]; @@ -93,6 +91,12 @@ export function resolveAppNodeSpecs(options: { ); } + const appPlugin = + plugins.find(plugin => plugin.id === 'app') ?? + createFrontendPlugin({ + pluginId: 'app', + }); + const configuredExtensions = [ ...pluginExtensions.map(({ plugin, ...extension }) => { const internalExtension = toInternalExtension(extension); @@ -112,8 +116,8 @@ export function resolveAppNodeSpecs(options: { return { extension: internalExtension, params: { - source: rootPlugin, - plugin: rootPlugin, + source: appPlugin, + plugin: appPlugin, attachTo: internalExtension.attachTo, disabled: internalExtension.disabled, config: undefined as unknown, diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index 3efd500a78..f1161b202e 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -38,7 +38,7 @@ const baseSpec = { extension, attachTo: { id: 'nonexistent', input: 'nonexistent' }, disabled: false, - plugin: createFrontendPlugin({ pluginId: 'root' }), + plugin: createFrontendPlugin({ pluginId: 'app' }), }; describe('buildAppTree', () => { diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 9dfc5cd7d7..9b9f337f4a 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -268,7 +268,7 @@ export interface AppNodeSpec { // (undocumented) readonly id: string; // (undocumented) - readonly plugin?: FrontendPlugin; + readonly plugin: FrontendPlugin; } // @public