frontend-plugin-api: add v2 extension and resolution

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-07-26 17:25:44 +02:00
parent ea25c0bf33
commit e2e84edf7b
2 changed files with 93 additions and 24 deletions
@@ -17,14 +17,54 @@
import { ExtensionDefinition } from './createExtension';
import { resolveExtensionDefinition } from './resolveExtensionDefinition';
const baseDef = {
$$type: '@backstage/ExtensionDefinition',
version: 'v1',
attachTo: { id: '', input: '' },
disabled: false,
};
describe('resolveExtensionDefinition', () => {
const baseDef = {
$$type: '@backstage/ExtensionDefinition',
version: 'v2',
attachTo: { id: '', input: '' },
disabled: false,
};
it.each([
[{ namespace: 'ns' }, 'ns'],
[{ namespace: 'n' }, 'n'],
[{ namespace: 'ns', name: 'n' }, 'ns/n'],
[{ kind: 'k', namespace: 'ns' }, 'k:ns'],
[{ kind: 'k', namespace: 'ns', name: 'n' }, 'k:ns/n'],
])(`should resolve extension IDs %s`, (definition, expected) => {
const resolved = resolveExtensionDefinition({
...baseDef,
...definition,
} as ExtensionDefinition<unknown>);
expect(resolved.id).toBe(expected);
expect(String(resolved)).toBe(`Extension{id=${expected}}`);
});
it('should fail to resolve extension ID without namespace', () => {
expect(() =>
resolveExtensionDefinition({
...baseDef,
kind: 'k',
} as ExtensionDefinition<unknown>),
).toThrow(
'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=k namespace=undefined name=undefined',
);
expect(() =>
resolveExtensionDefinition(baseDef as ExtensionDefinition<unknown>),
).toThrow(
'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=undefined namespace=undefined name=undefined',
);
});
});
describe('old resolveExtensionDefinition', () => {
const baseDef = {
$$type: '@backstage/ExtensionDefinition',
version: 'v1',
attachTo: { id: '', input: '' },
disabled: false,
};
it.each([
[{ namespace: 'ns' }, 'ns'],
[{ namespace: 'n' }, 'n'],
@@ -24,6 +24,11 @@ import {
toInternalExtensionDefinition,
} from './createExtension';
import { PortableSchema } from '../schema';
import { ExtensionInput } from './createExtensionInput';
import {
AnyExtensionDataRef,
ExtensionDataValue,
} from './createExtensionDataRef';
/** @public */
export interface Extension<TConfig, TConfigInput = TConfig> {
@@ -35,17 +40,42 @@ export interface Extension<TConfig, TConfigInput = TConfig> {
}
/** @internal */
export interface InternalExtension<TConfig, TConfigInput>
extends Extension<TConfig, TConfigInput> {
readonly version: 'v1';
readonly inputs: AnyExtensionInputMap;
readonly output: AnyExtensionDataMap;
factory(options: {
node: AppNode;
config: TConfig;
inputs: ResolvedExtensionInputs<any>;
}): ExtensionDataValues<any>;
}
export type InternalExtension<TConfig, TConfigInput> = Extension<
TConfig,
TConfigInput
> &
(
| {
readonly version: 'v1';
readonly inputs: AnyExtensionInputMap;
readonly output: AnyExtensionDataMap;
factory(options: {
node: AppNode;
config: TConfig;
inputs: ResolvedExtensionInputs<AnyExtensionInputMap>;
}): ExtensionDataValues<any>;
}
| {
readonly version: 'v2';
readonly inputs: {
[inputName in string]: ExtensionInput<
AnyExtensionDataRef,
{ optional: boolean; singleton: boolean }
>;
};
readonly output: Array<AnyExtensionDataRef>;
factory(options: {
node: AppNode;
config: TConfig;
inputs: ResolvedExtensionInputs<{
[inputName in string]: ExtensionInput<
AnyExtensionDataRef,
{ optional: boolean; singleton: boolean }
>;
}>;
}): Iterable<ExtensionDataValue<any, any>>;
}
);
/** @internal */
export function toInternalExtension<TConfig, TConfigInput>(
@@ -57,10 +87,9 @@ export function toInternalExtension<TConfig, TConfigInput>(
`Invalid extension instance, bad type '${internal.$$type}'`,
);
}
if (internal.version !== 'v1') {
throw new Error(
`Invalid extension instance, bad version '${internal.version}'`,
);
const version = internal.version;
if (version !== 'v1' && version !== 'v2') {
throw new Error(`Invalid extension instance, bad version '${version}'`);
}
return internal;
}
@@ -87,10 +116,10 @@ export function resolveExtensionDefinition<TConfig, TConfigInput>(
return {
...rest,
$$type: '@backstage/Extension',
version: 'v1',
version: internalDefinition.version,
id,
toString() {
return `Extension{id=${id}}`;
},
} as Extension<TConfig, TConfigInput>;
} as InternalExtension<TConfig, TConfigInput>;
}