Simplify the ExtensionAttachTo type

Simplified the `ExtensionAttachTo` type to only support a single
attachment target, removing the array form for attaching to multiple
extension points. Also removed the deprecated `ExtensionAttachToSpec`
type alias.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-05 10:37:05 +01:00
parent d0b53e39fd
commit a9440f0622
9 changed files with 68 additions and 226 deletions
+5 -13
View File
@@ -924,7 +924,7 @@ export interface Extension<TConfig, TConfigInput = TConfig> {
// (undocumented)
$$type: '@backstage/Extension';
// (undocumented)
readonly attachTo: ExtensionAttachToSpec;
readonly attachTo: ExtensionAttachTo;
// (undocumented)
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
// (undocumented)
@@ -934,18 +934,10 @@ export interface Extension<TConfig, TConfigInput = TConfig> {
}
// @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<
@@ -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 '<detached-input>';
}
let id = '<plugin>';
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 = '<detached-input>';
} else {
let id = '<plugin>';
if (context?.kind) {
id = `${context?.kind}:${id}`;
}
if ('relative' in a && a.relative) {
let id = '<plugin>';
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 = '<plugin>';
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(',')}}`;
},
@@ -58,7 +58,6 @@ export {
export {
type Extension,
type ExtensionAttachTo,
type ExtensionAttachToSpec,
} from './resolveExtensionDefinition';
export {
type ExtensionDataContainer,
@@ -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',
},
]);
});
});
@@ -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<TConfig, TConfigInput = TConfig> {
$$type: '@backstage/Extension';
readonly id: string;
readonly attachTo: ExtensionAttachToSpec;
readonly attachTo: ExtensionAttachTo;
readonly disabled: boolean;
readonly configSchema?: PortableSchema<TConfig, TConfigInput>;
}
@@ -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 */