diff --git a/.changeset/simplify-extension-attach-to.md b/.changeset/simplify-extension-attach-to.md
new file mode 100644
index 0000000000..2beb00d056
--- /dev/null
+++ b/.changeset/simplify-extension-attach-to.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-plugin-api': minor
+---
+
+**BREAKING**: Simplified the `ExtensionAttachTo` type to only support a single attachment target. The array form for attaching to multiple extension points has been removed. Also removed the deprecated `ExtensionAttachToSpec` type alias.
diff --git a/packages/core-compat-api/src/collectEntityPageContents.test.tsx b/packages/core-compat-api/src/collectEntityPageContents.test.tsx
index fd74a7e89b..5ee1439360 100644
--- a/packages/core-compat-api/src/collectEntityPageContents.test.tsx
+++ b/packages/core-compat-api/src/collectEntityPageContents.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ExtensionAttachToSpec } from '@backstage/frontend-plugin-api';
+import { ExtensionAttachTo } from '@backstage/frontend-plugin-api';
import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog';
import { JSX } from 'react';
import { collectEntityPageContents } from './collectEntityPageContents';
@@ -73,7 +73,7 @@ const otherTestContent = (
function collect(element: JSX.Element) {
const result = new Array<{
id: string;
- attachTo: ExtensionAttachToSpec;
+ attachTo: ExtensionAttachTo;
}>();
collectEntityPageContents(element, {
diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts
index bffc156509..80d478646f 100644
--- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts
+++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts
@@ -137,60 +137,6 @@ describe('buildAppTree', () => {
`);
});
- it('should create a tree with clones', () => {
- const tree = resolveAppTree(
- 'a',
- [
- { ...baseSpec, id: 'a' },
- { ...baseSpec, id: 'b', attachTo: { id: 'a', input: 'x' } },
- {
- ...baseSpec,
- id: 'c',
- attachTo: [
- { id: 'a', input: 'x' },
- { id: 'b', input: 'x' },
- ],
- },
- {
- ...baseSpec,
- id: 'd',
- attachTo: [
- { id: 'b', input: 'x' },
- { id: 'c', input: 'x' },
- ],
- },
- ],
- collector,
- );
-
- expect(Array.from(tree.nodes.keys())).toEqual(['a', 'b', 'c', 'd']);
-
- expect(String(tree.root)).toMatchInlineSnapshot(`
- "
- x [
-
- x [
-
- x [
-
- ]
-
-
- ]
-
-
- x [
-
- ]
-
- ]
- "
- `);
-
- const orphans = Array.from(tree.orphans).map(String);
- expect(orphans).toMatchInlineSnapshot(`[]`);
- });
-
it('should create a tree out of order', () => {
const tree = resolveAppTree(
'b',
diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.ts b/packages/frontend-app-api/src/tree/resolveAppTree.ts
index c2054c4bac..bdd9be0fbf 100644
--- a/packages/frontend-app-api/src/tree/resolveAppTree.ts
+++ b/packages/frontend-app-api/src/tree/resolveAppTree.ts
@@ -153,7 +153,6 @@ export function resolveAppTree(
}
const orphans = new Array();
- const clones = new Map>();
// A node with the provided rootNodeId must be found in the tree, and it must not be attached to anything
let rootNode: AppNode | undefined = undefined;
@@ -164,46 +163,6 @@ export function resolveAppTree(
// TODO: For now we simply ignore the attachTo spec of the root node, but it'd be cleaner if we could avoid defining it
if (spec.id === rootNodeId) {
rootNode = node;
- } else if (Array.isArray(spec.attachTo)) {
- // eslint-disable-next-line no-console
- console.warn(
- `Extension '${spec.id}' is using multiple attachment points which is 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.`,
- );
- let foundFirstParent = false;
- for (const origAttachTo of spec.attachTo) {
- let attachTo = origAttachTo;
-
- if (!isValidAttachmentPoint(attachTo, nodes)) {
- attachTo =
- redirectTargetsByKey.get(makeRedirectKey(attachTo)) ?? attachTo;
- }
-
- const parent = nodes.get(attachTo.id);
- if (parent) {
- const cloneParents = clones.get(attachTo.id) ?? [];
-
- if (!foundFirstParent) {
- foundFirstParent = true;
- node.setParent(parent, attachTo.input);
- } else {
- cloneParents.unshift(parent);
- }
-
- for (const extraParent of cloneParents) {
- const clonedNode = new SerializableAppNode(spec);
- clonedNode.setParent(extraParent, attachTo.input);
- clones.set(
- spec.id,
- clones.get(spec.id)?.concat(clonedNode) ?? [clonedNode],
- );
- }
- }
- }
- if (!foundFirstParent) {
- orphans.push(node);
- }
} else {
let attachTo = spec.attachTo;
if (!isValidAttachmentPoint(attachTo, nodes)) {
diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md
index c83511fc47..83c4035e3c 100644
--- a/packages/frontend-plugin-api/report.api.md
+++ b/packages/frontend-plugin-api/report.api.md
@@ -924,7 +924,7 @@ export interface Extension {
// (undocumented)
$$type: '@backstage/Extension';
// (undocumented)
- readonly attachTo: ExtensionAttachToSpec;
+ readonly attachTo: ExtensionAttachTo;
// (undocumented)
readonly configSchema?: PortableSchema;
// (undocumented)
@@ -934,18 +934,10 @@ export interface Extension {
}
// @public (undocumented)
-export type ExtensionAttachTo =
- | {
- id: string;
- input: string;
- }
- | Array<{
- id: string;
- input: string;
- }>;
-
-// @public @deprecated (undocumented)
-export type ExtensionAttachToSpec = ExtensionAttachTo;
+export type ExtensionAttachTo = {
+ id: string;
+ input: string;
+};
// @public (undocumented)
export interface ExtensionBlueprint<
diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts
index 4d3eeece2c..42611e78ca 100644
--- a/packages/frontend-plugin-api/src/wiring/createExtension.ts
+++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts
@@ -488,40 +488,36 @@ export function createExtension<
if (options.name) {
parts.push(`name=${options.name}`);
}
- const attachTo = [options.attachTo]
- .flat()
- .map(aAny => {
- const a = aAny as ExtensionDefinitionAttachTo;
- if (OpaqueExtensionInput.isType(a)) {
- const { context } = OpaqueExtensionInput.toInternal(a);
- if (!context) {
- return '';
- }
- let id = '';
- if (context?.kind) {
- id = `${context?.kind}:${id}`;
- }
- if (context?.name) {
- id = `${id}/${context?.name}`;
- }
- return `${id}@${context.input}`;
+ const a = options.attachTo;
+ let attachTo: string;
+ if (OpaqueExtensionInput.isType(a)) {
+ const { context } = OpaqueExtensionInput.toInternal(a);
+ if (!context) {
+ attachTo = '';
+ } else {
+ let id = '';
+ if (context?.kind) {
+ id = `${context?.kind}:${id}`;
}
- if ('relative' in a && a.relative) {
- let id = '';
- if (a.relative.kind) {
- id = `${a.relative.kind}:${id}`;
- }
- if (a.relative.name) {
- id = `${id}/${a.relative.name}`;
- }
- return `${id}@${a.input}`;
+ if (context?.name) {
+ id = `${id}/${context?.name}`;
}
- if ('id' in a) {
- return `${a.id}@${a.input}`;
- }
- throw new Error('Invalid attachment point specification');
- })
- .join('+');
+ attachTo = `${id}@${context.input}`;
+ }
+ } else if ('relative' in a && a.relative) {
+ let id = '';
+ if (a.relative.kind) {
+ id = `${a.relative.kind}:${id}`;
+ }
+ if (a.relative.name) {
+ id = `${id}/${a.relative.name}`;
+ }
+ attachTo = `${id}@${a.input}`;
+ } else if ('id' in a) {
+ attachTo = `${a.id}@${a.input}`;
+ } else {
+ throw new Error('Invalid attachment point specification');
+ }
parts.push(`attachTo=${attachTo}`);
return `ExtensionDefinition{${parts.join(',')}}`;
},
diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts
index 6ce1bb4998..3ec4631543 100644
--- a/packages/frontend-plugin-api/src/wiring/index.ts
+++ b/packages/frontend-plugin-api/src/wiring/index.ts
@@ -58,7 +58,6 @@ export {
export {
type Extension,
type ExtensionAttachTo,
- type ExtensionAttachToSpec,
} from './resolveExtensionDefinition';
export {
type ExtensionDataContainer,
diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts
index e080ab1f64..79275ab03c 100644
--- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts
+++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts
@@ -139,43 +139,6 @@ describe('resolveExtensionDefinition', () => {
id: 'test',
input: 'children',
});
-
- // Test for backward compatibility - runtime still supports multiple attachment points
- expect(
- resolveExtensionDefinition(
- OpaqueExtensionDefinition.toInternal({
- ...baseDef,
- attachTo: [
- baseInpuf.withContext?.({
- kind: 'k1',
- input: 'children',
- }),
- baseInpuf.withContext?.({
- kind: 'k2',
- input: 'children',
- }),
- baseInpuf.withContext?.({
- kind: 'k3',
- input: 'children',
- }),
- ] as any,
- }),
- { namespace: 'test' },
- ).attachTo,
- ).toEqual([
- {
- id: 'k1:test',
- input: 'children',
- },
- {
- id: 'k2:test',
- input: 'children',
- },
- {
- id: 'k3:test',
- input: 'children',
- },
- ]);
});
});
diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
index 605e75bfe8..a314983b25 100644
--- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
+++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts
@@ -30,21 +30,13 @@ import {
} from '@internal/frontend';
/** @public */
-export type ExtensionAttachTo =
- | { id: string; input: string }
- | Array<{ id: string; input: string }>;
-
-/**
- * @deprecated Use {@link ExtensionAttachTo} instead.
- * @public
- */
-export type ExtensionAttachToSpec = ExtensionAttachTo;
+export type ExtensionAttachTo = { id: string; input: string };
/** @public */
export interface Extension {
$$type: '@backstage/Extension';
readonly id: string;
- readonly attachTo: ExtensionAttachToSpec;
+ readonly attachTo: ExtensionAttachTo;
readonly disabled: boolean;
readonly configSchema?: PortableSchema;
}
@@ -149,45 +141,35 @@ function resolveExtensionId(
}
function resolveAttachTo(
- attachTo: ExtensionDefinitionAttachTo | ExtensionDefinitionAttachTo[],
+ attachTo: ExtensionDefinitionAttachTo,
namespace?: string,
-): ExtensionAttachToSpec {
- const resolveSpec = (
- spec: ExtensionDefinitionAttachTo,
- ): { id: string; input: string } => {
- if (OpaqueExtensionInput.isType(spec)) {
- const { context } = OpaqueExtensionInput.toInternal(spec);
- if (!context) {
- throw new Error(
- 'Invalid input object without a parent extension used as attachment point',
- );
- }
- return {
- id: resolveExtensionId(context.kind, namespace, context.name),
- input: context.input,
- };
+): ExtensionAttachTo {
+ if (OpaqueExtensionInput.isType(attachTo)) {
+ const { context } = OpaqueExtensionInput.toInternal(attachTo);
+ if (!context) {
+ throw new Error(
+ 'Invalid input object without a parent extension used as attachment point',
+ );
}
- if ('relative' in spec && spec.relative) {
- return {
- id: resolveExtensionId(
- spec.relative.kind,
- namespace,
- spec.relative.name,
- ),
- input: spec.input,
- };
- }
- if ('id' in spec) {
- return { id: spec.id, input: spec.input };
- }
- throw new Error('Invalid attachment point specification');
- };
-
- if (Array.isArray(attachTo)) {
- return attachTo.map(resolveSpec);
+ return {
+ id: resolveExtensionId(context.kind, namespace, context.name),
+ input: context.input,
+ };
}
-
- return resolveSpec(attachTo);
+ if ('relative' in attachTo && attachTo.relative) {
+ return {
+ id: resolveExtensionId(
+ attachTo.relative.kind,
+ namespace,
+ attachTo.relative.name,
+ ),
+ input: attachTo.input,
+ };
+ }
+ if ('id' in attachTo) {
+ return { id: attachTo.id, input: attachTo.input };
+ }
+ throw new Error('Invalid attachment point specification');
}
/** @internal */