Merge pull request #32626 from backstage/rugvip/remove-multi-attachto-types

frontend-plugin-api: remove type support for multiple attachment points
This commit is contained in:
Patrik Oldsberg
2026-02-03 12:17:06 +01:00
committed by GitHub
10 changed files with 41 additions and 73 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/frontend-plugin-api': minor
---
**BREAKING**: Removed type support for multiple attachment points in the `ExtensionDefinitionAttachTo` type. Extensions can no longer specify an array of attachment points in the `attachTo` property.
The runtime still supports multiple attachment points for backward compatibility with existing compiled code, but new code will receive type errors if attempting to use this pattern.
Extensions that previously used multiple attachment points should migrate to using a Utility API pattern instead. See the [Sharing Extensions Across Multiple Locations](https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions) guide for the recommended approach.
@@ -648,12 +648,13 @@ describe('createSpecializedApp', () => {
output: [coreExtensionData.reactElement],
factory: () => [coreExtensionData.reactElement(<div />)],
}),
// Test backward compatibility - runtime still supports multiple attachment points
createExtension({
name: 'cloned',
attachTo: [
{ id: 'test/a', input: 'children' },
{ id: 'test/b', input: 'children' },
],
] as any,
output: [coreExtensionData.reactElement],
factory: () => [coreExtensionData.reactElement(<div />)],
}),
+1 -20
View File
@@ -1205,26 +1205,7 @@ export type ExtensionDefinitionAttachTo<
input: string;
id?: never;
}
| ExtensionInput<UParentInputs>
/**
* @deprecated Multiple attachment points are deprecated and will be removed in a future release. Use a Utility API instead to share functionality across multiple locations. See https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions for migration guidance.
*/
| Array<
| {
id: string;
input: string;
relative?: never;
}
| {
relative: {
kind?: string;
name?: string;
};
input: string;
id?: never;
}
| ExtensionInput<UParentInputs>
>;
| ExtensionInput<UParentInputs>;
// @public (undocumented)
export type ExtensionDefinitionParameters = {
@@ -32,12 +32,10 @@ describe('AnalyticsBlueprint', () => {
{
"$$type": "@backstage/ExtensionDefinition",
"T": undefined,
"attachTo": [
{
"id": "api:app/analytics",
"input": "implementations",
},
],
"attachTo": {
"id": "api:app/analytics",
"input": "implementations",
},
"configSchema": undefined,
"disabled": false,
"factory": [Function],
@@ -41,7 +41,7 @@ const factoryDataRef =
*/
export const AnalyticsImplementationBlueprint = createExtensionBlueprint({
kind: 'analytics',
attachTo: [{ id: 'api:app/analytics', input: 'implementations' }],
attachTo: { id: 'api:app/analytics', input: 'implementations' },
output: [factoryDataRef],
dataRefs: {
factory: factoryDataRef,
@@ -202,12 +202,14 @@ describe('createExtension', () => {
});
});
it('should create an extension with multiple attachment points', () => {
// Tests for backward compatibility - runtime still supports multiple attachment points
// but the TypeScript types no longer allow them
it('should create an extension with multiple attachment points (backward compat)', () => {
const extension = createExtension({
attachTo: [
{ id: 'root', input: 'default' },
{ id: 'other', input: 'default' },
],
] as any,
output: [stringDataRef, numberDataRef.optional()],
factory: () => [stringDataRef('bar')],
});
@@ -216,14 +218,14 @@ describe('createExtension', () => {
);
});
it('should create an extension with relative attachment points', () => {
it('should create an extension with relative attachment points (backward compat)', () => {
const extension = createExtension({
attachTo: [
{ relative: {}, input: 'tabs' },
{ relative: { kind: 'page' }, input: 'tabs' },
{ relative: { name: 'index' }, input: 'tabs' },
{ relative: { kind: 'page', name: 'index' }, input: 'tabs' },
],
] as any,
output: [stringDataRef],
factory: () => [stringDataRef('bar')],
});
@@ -232,7 +234,7 @@ describe('createExtension', () => {
);
});
it('should create an extension with relative attachment points by reference', () => {
it('should create an extension with relative attachment points by reference (backward compat)', () => {
const baseOpts = {
attachTo: { id: 'root', input: 'children' },
inputs: {
@@ -269,7 +271,7 @@ describe('createExtension', () => {
parent2.inputs.tabs,
parent3.inputs.tabs,
parent4.inputs.otherTabs,
],
] as any,
output: [stringDataRef],
factory: () => [stringDataRef('bar')],
});
@@ -281,7 +283,7 @@ describe('createExtension', () => {
parent2.inputs.tabs,
parent3.inputs.tabs,
parent4.inputs.otherTabs,
],
] as any,
});
expect(String(overrdeExtension)).toBe(
'ExtensionDefinition{attachTo=page:<plugin>@tabs+<plugin>/index@tabs+page:<plugin>/index@otherTabs}',
@@ -134,11 +134,10 @@ export type VerifyExtensionAttachTo<
*
* A standard attachment point declaration will specify the ID of the parent extension, as well as the name of the input to attach to.
*
* There are three more advanced forms that are available for more complex use-cases:
* There are two more advanced forms that are available for more complex use-cases:
*
* 1. Relative attachment points: using the `relative` property instead of `id`, the attachment point is resolved relative to the current plugin.
* 2. Extension input references: using a reference in code to another extension's input in the same plugin. These references are always relative.
* 3. Array of attachment points: an array of attachment points can be used to clone and attach to multiple extensions at once.
*
* @example
* ```ts
@@ -151,12 +150,6 @@ export type VerifyExtensionAttachTo<
* // Attach to a specific input of another extension
* const page = ParentBlueprint.make({ ... });
* const child = ChildBlueprint.make({ attachTo: page.inputs.children });
*
* // Attach to multiple parents at once (deprecated - use Utility APIs instead)
* [
* { id: 'page/home', input: 'widgets' },
* { relative: { kind: 'page' }, input: 'widgets' },
* ]
* ```
*
* @public
@@ -166,19 +159,7 @@ export type ExtensionDefinitionAttachTo<
> =
| { id: string; input: string; relative?: never }
| { relative: { kind?: string; name?: string }; input: string; id?: never }
| ExtensionInput<UParentInputs>
/**
* @deprecated Multiple attachment points are deprecated and will be removed in a future release. Use a Utility API instead to share functionality across multiple locations. See https://backstage.io/docs/frontend-system/architecture/27-sharing-extensions for migration guidance.
*/
| Array<
| { id: string; input: string; relative?: never }
| {
relative: { kind?: string; name?: string };
input: string;
id?: never;
}
| ExtensionInput<UParentInputs>
>;
| ExtensionInput<UParentInputs>;
/** @public */
export type CreateExtensionOptions<
@@ -92,11 +92,7 @@ describe('createExtensionBlueprint', () => {
it('should allow creation of extension blueprints with a generator', () => {
const TestExtensionBlueprint = createExtensionBlueprint({
kind: 'test-extension',
// Try multiple attachment points for this one
attachTo: [
{ id: 'test-1', input: 'default' },
{ id: 'test-2', input: 'default' },
],
attachTo: { id: 'test-1', input: 'default' },
output: [coreExtensionData.reactElement],
*factory(params: { text: string }) {
yield coreExtensionData.reactElement(<h1>{params.text}</h1>);
@@ -112,10 +108,7 @@ describe('createExtensionBlueprint', () => {
expect(extension).toEqual({
$$type: '@backstage/ExtensionDefinition',
attachTo: [
{ id: 'test-1', input: 'default' },
{ id: 'test-2', input: 'default' },
],
attachTo: { id: 'test-1', input: 'default' },
configSchema: undefined,
disabled: false,
inputs: {},
@@ -1345,7 +1338,9 @@ describe('createExtensionBlueprint', () => {
).toThrow('Refused to override params and factory at the same time');
});
describe('with relative attachment points', () => {
// Tests for backward compatibility - runtime still supports multiple attachment points
// but the TypeScript types no longer allow them
describe('with relative attachment points (backward compat)', () => {
const dataRef = createExtensionDataRef<string>().with({ id: 'test.data' });
it('should create an extension with relative attachment points', () => {
@@ -1356,7 +1351,7 @@ describe('createExtensionBlueprint', () => {
{ relative: { kind: 'page' }, input: 'tabs' },
{ relative: { name: 'index' }, input: 'tabs' },
{ relative: { kind: 'page', name: 'index' }, input: 'tabs' },
],
] as any,
output: [dataRef],
factory: () => [dataRef('bar')],
});
@@ -1371,7 +1366,7 @@ describe('createExtensionBlueprint', () => {
{ relative: { kind: 'page' }, input: 'tabs' },
{ relative: { name: 'index' }, input: 'tabs' },
{ relative: { kind: 'page', name: 'index' }, input: 'tabs' },
],
] as any,
params: {},
}),
),
@@ -1431,7 +1426,7 @@ describe('createExtensionBlueprint', () => {
parent2.inputs.tabs,
parent3.inputs.tabs,
parent4.inputs.otherTabs,
],
] as any,
output: [dataRef],
factory: () => [dataRef('bar')],
});
@@ -1445,7 +1440,7 @@ describe('createExtensionBlueprint', () => {
parent2.inputs.tabs,
parent3.inputs.tabs,
parent4.inputs.otherTabs,
],
] as any,
params: {},
}),
),
@@ -140,6 +140,7 @@ describe('resolveExtensionDefinition', () => {
input: 'children',
});
// Test for backward compatibility - runtime still supports multiple attachment points
expect(
resolveExtensionDefinition(
OpaqueExtensionDefinition.toInternal({
@@ -157,7 +158,7 @@ describe('resolveExtensionDefinition', () => {
kind: 'k3',
input: 'children',
}),
],
] as any,
}),
{ namespace: 'test' },
).attachTo,
@@ -149,11 +149,11 @@ function resolveExtensionId(
}
function resolveAttachTo(
attachTo: ExtensionDefinitionAttachTo,
attachTo: ExtensionDefinitionAttachTo | ExtensionDefinitionAttachTo[],
namespace?: string,
): ExtensionAttachToSpec {
const resolveSpec = (
spec: Exclude<ExtensionDefinitionAttachTo, Array<any>>,
spec: ExtensionDefinitionAttachTo,
): { id: string; input: string } => {
if (OpaqueExtensionInput.isType(spec)) {
const { context } = OpaqueExtensionInput.toInternal(spec);