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:
@@ -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.
|
||||
@@ -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, {
|
||||
|
||||
@@ -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(`
|
||||
"<a>
|
||||
x [
|
||||
<b>
|
||||
x [
|
||||
<c>
|
||||
x [
|
||||
<d />
|
||||
]
|
||||
</c>
|
||||
<d />
|
||||
]
|
||||
</b>
|
||||
<c>
|
||||
x [
|
||||
<d />
|
||||
]
|
||||
</c>
|
||||
]
|
||||
</a>"
|
||||
`);
|
||||
|
||||
const orphans = Array.from(tree.orphans).map(String);
|
||||
expect(orphans).toMatchInlineSnapshot(`[]`);
|
||||
});
|
||||
|
||||
it('should create a tree out of order', () => {
|
||||
const tree = resolveAppTree(
|
||||
'b',
|
||||
|
||||
@@ -153,7 +153,6 @@ export function resolveAppTree(
|
||||
}
|
||||
|
||||
const orphans = new Array<SerializableAppNode>();
|
||||
const clones = new Map<string, Array<SerializableAppNode>>();
|
||||
|
||||
// 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)) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user