Merge pull request #30816 from backstage/camilaibs/make-app-node-spec-plugin-required

[NFS] frontend-plugin-api: make app spec plugin required
This commit is contained in:
Camila Belo
2025-08-11 10:33:34 +02:00
committed by GitHub
8 changed files with 103 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': minor
---
Use an app plugin for built-in extension app node specs.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': minor
---
The `AppNodeSpec.plugin` property is now required.
@@ -27,6 +27,7 @@ import {
createExtensionBlueprint,
createExtensionDataRef,
createExtensionInput,
createFrontendPlugin,
} from '@backstage/frontend-plugin-api';
import {
createAppNodeInstance,
@@ -59,7 +60,7 @@ function makeSpec<TConfig, TConfigInput>(
attachTo: extension.attachTo,
disabled: extension.disabled,
extension: extension as Extension<unknown, unknown>,
plugin: undefined,
plugin: createFrontendPlugin({ pluginId: 'app' }),
...spec,
};
}
@@ -68,6 +68,9 @@ describe('resolveAppNodeSpecs', () => {
extension: a,
attachTo: { id: 'root', input: 'default' },
disabled: true,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
]);
});
@@ -87,12 +90,18 @@ describe('resolveAppNodeSpecs', () => {
extension: a,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'b',
extension: b,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
]);
});
@@ -120,6 +129,9 @@ describe('resolveAppNodeSpecs', () => {
extension: b,
attachTo: { id: 'derp', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'test/a',
@@ -204,12 +216,18 @@ describe('resolveAppNodeSpecs', () => {
extension: b,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'a',
extension: a,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
]);
});
@@ -238,42 +256,63 @@ describe('resolveAppNodeSpecs', () => {
extension: e,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'd',
extension: d,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'c',
extension: c,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'a',
extension: a,
attachTo: { id: 'root', input: 'default' },
disabled: true,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'b',
extension: b,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'f',
extension: f,
attachTo: { id: 'root', input: 'default' },
disabled: false,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
{
id: 'g',
extension: g,
attachTo: { id: 'root', input: 'default' },
disabled: true,
config: undefined,
plugin: expect.any(Object),
source: expect.any(Object),
},
]);
});
@@ -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';
@@ -87,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);
@@ -106,8 +116,8 @@ export function resolveAppNodeSpecs(options: {
return {
extension: internalExtension,
params: {
source: undefined,
plugin: undefined,
source: appPlugin,
plugin: appPlugin,
attachTo: internalExtension.attachTo,
disabled: internalExtension.disabled,
config: undefined as unknown,
@@ -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: 'app' }),
};
describe('buildAppTree', () => {
@@ -284,8 +286,20 @@ describe('buildAppTree', () => {
) as Extension<unknown, unknown>;
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<unknown, unknown>;
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(
+1 -1
View File
@@ -268,7 +268,7 @@ export interface AppNodeSpec {
// (undocumented)
readonly id: string;
// (undocumented)
readonly plugin?: FrontendPlugin;
readonly plugin: FrontendPlugin;
}
// @public
@@ -37,7 +37,7 @@ export interface AppNodeSpec {
readonly extension: Extension<unknown, unknown>;
readonly disabled: boolean;
readonly config?: unknown;
readonly plugin?: FrontendPlugin;
readonly plugin: FrontendPlugin;
}
/**