frontend-plugin-api: refactor to use two disctict internal extension definitions
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -40,7 +40,7 @@ describe('createExtension', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.namespace).toBe('test');
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
|
||||
// When declared as an error function without a block the TypeScript errors
|
||||
// are a more specific and will often point at the property that is problematic.
|
||||
@@ -177,7 +177,7 @@ describe('createExtension', () => {
|
||||
foo: 'bar',
|
||||
}),
|
||||
});
|
||||
expect(extension.namespace).toBe('test');
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
@@ -287,7 +287,7 @@ describe('createExtension', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.namespace).toBe('test');
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
expect(String(extension)).toBe(
|
||||
'ExtensionDefinition{namespace=test,attachTo=root@default}',
|
||||
);
|
||||
@@ -325,7 +325,7 @@ describe('createExtension', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.namespace).toBe('test');
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
expect(String(extension)).toBe(
|
||||
'ExtensionDefinition{namespace=test,attachTo=root@default}',
|
||||
);
|
||||
@@ -358,98 +358,108 @@ describe('createExtension', () => {
|
||||
});
|
||||
|
||||
it('should support new form of outputs', () => {
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef],
|
||||
factory() {
|
||||
return []; // Missing all outputs
|
||||
},
|
||||
});
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef],
|
||||
factory() {
|
||||
return []; // Missing all outputs
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef],
|
||||
factory() {
|
||||
return [stringDataRef('hello')]; // Missing number output
|
||||
},
|
||||
});
|
||||
expect(
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef],
|
||||
factory() {
|
||||
return [stringDataRef('hello')]; // Missing number output
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
|
||||
// Duplicate output, we won't attempt to handle this a compile time and instead error out at runtime
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef],
|
||||
factory() {
|
||||
return [stringDataRef('hello'), stringDataRef('hello')];
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef],
|
||||
factory() {
|
||||
return [stringDataRef('hello'), stringDataRef('hello')];
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef],
|
||||
factory() {
|
||||
return [stringDataRef('hello'), numberDataRef(4)];
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef],
|
||||
factory() {
|
||||
return [stringDataRef('hello'), numberDataRef(4)];
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef.optional()],
|
||||
factory() {
|
||||
return [stringDataRef('hello'), numberDataRef(4)];
|
||||
},
|
||||
});
|
||||
expect(
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef.optional()],
|
||||
factory() {
|
||||
return [stringDataRef('hello'), numberDataRef(4)];
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef.optional()],
|
||||
factory() {
|
||||
return [stringDataRef('hello')]; // Missing number output, but it's optional so that's allowed
|
||||
},
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
expect(
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: [stringDataRef, numberDataRef.optional()],
|
||||
factory() {
|
||||
return [stringDataRef('hello')]; // Missing number output, but it's optional so that's allowed
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
});
|
||||
|
||||
it('should support new form of inputs', () => {
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
header: createExtensionInput([stringDataRef.optional()], {
|
||||
optional: true,
|
||||
singleton: true,
|
||||
}),
|
||||
content: createExtensionInput([stringDataRef, numberDataRef], {
|
||||
optional: false,
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
output: [stringDataRef],
|
||||
factory({ inputs }) {
|
||||
const headerStr = inputs.header?.get(stringDataRef);
|
||||
const contentStr = inputs.content.get(stringDataRef);
|
||||
const contentNum = inputs.content.get(numberDataRef);
|
||||
expect(
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
header: createExtensionInput([stringDataRef.optional()], {
|
||||
optional: true,
|
||||
singleton: true,
|
||||
}),
|
||||
content: createExtensionInput([stringDataRef, numberDataRef], {
|
||||
optional: false,
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
output: [stringDataRef],
|
||||
factory({ inputs }) {
|
||||
const headerStr = inputs.header?.get(stringDataRef);
|
||||
const contentStr = inputs.content.get(stringDataRef);
|
||||
const contentNum = inputs.content.get(numberDataRef);
|
||||
|
||||
// @ts-expect-error
|
||||
inputs.header?.get(numberDataRef);
|
||||
// @ts-expect-error
|
||||
inputs.header?.get(numberDataRef);
|
||||
|
||||
// @ts-expect-error
|
||||
const x1: string = headerStr; // string | undefined
|
||||
// @ts-expect-error
|
||||
const x1: string = headerStr; // string | undefined
|
||||
|
||||
unused(x1);
|
||||
unused(x1);
|
||||
|
||||
return [stringDataRef(contentStr.repeat(contentNum))];
|
||||
},
|
||||
});
|
||||
|
||||
expect(true).toBe(true);
|
||||
return [stringDataRef(contentStr.repeat(contentNum))];
|
||||
},
|
||||
}),
|
||||
).toMatchObject({ version: 'v2' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
ExtensionDataRef,
|
||||
ExtensionDataValue,
|
||||
} from './createExtensionDataRef';
|
||||
import { ExtensionInput } from './createExtensionInput';
|
||||
import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
@@ -33,10 +33,12 @@ export type AnyExtensionDataMap = {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* @public
|
||||
* @deprecated This type will be removed.
|
||||
*/
|
||||
export type AnyExtensionInputMap = {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
[inputName in string]: LegacyExtensionInput<
|
||||
AnyExtensionDataMap,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
@@ -95,7 +97,7 @@ export type ResolvedExtensionInput<
|
||||
* @public
|
||||
*/
|
||||
export type ResolvedExtensionInputs<
|
||||
TInputs extends { [name in string]: ExtensionInput<any, any, any> },
|
||||
TInputs extends { [name in string]: ExtensionInput<any, any> },
|
||||
> = {
|
||||
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
|
||||
? Array<Expand<ResolvedExtensionInput<TInputs[InputName]['extensionData']>>>
|
||||
@@ -146,7 +148,12 @@ export interface LegacyCreateExtensionOptions<
|
||||
/** @public */
|
||||
export type CreateExtensionOptions<
|
||||
UOutput extends AnyExtensionDataRef,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
|
||||
@@ -205,17 +212,40 @@ export interface ExtensionDefinition<TConfig, TConfigInput = TConfig> {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalExtensionDefinition<TConfig, TConfigInput>
|
||||
extends ExtensionDefinition<TConfig, TConfigInput> {
|
||||
readonly version: 'v1';
|
||||
readonly inputs: AnyExtensionInputMap;
|
||||
readonly output: AnyExtensionDataMap | Array<AnyExtensionDataRef>;
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: ResolvedExtensionInputs<any>;
|
||||
}): ExtensionDataValues<any> | Iterable<ExtensionDataValue<any, any>>;
|
||||
}
|
||||
export type InternalExtensionDefinition<TConfig, TConfigInput> =
|
||||
ExtensionDefinition<TConfig, TConfigInput> &
|
||||
(
|
||||
| {
|
||||
readonly version: 'v1';
|
||||
readonly inputs: AnyExtensionInputMap;
|
||||
readonly output: AnyExtensionDataMap;
|
||||
factory(context: {
|
||||
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(context: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: ResolvedExtensionInputs<{
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
}>;
|
||||
}): Iterable<ExtensionDataValue<any, any>>;
|
||||
}
|
||||
);
|
||||
|
||||
/** @internal */
|
||||
export function toInternalExtensionDefinition<TConfig, TConfigInput>(
|
||||
@@ -230,9 +260,10 @@ export function toInternalExtensionDefinition<TConfig, TConfigInput>(
|
||||
`Invalid extension definition instance, bad type '${internal.$$type}'`,
|
||||
);
|
||||
}
|
||||
if (internal.version !== 'v1') {
|
||||
const version = internal.version;
|
||||
if (version !== 'v1' && version !== 'v2') {
|
||||
throw new Error(
|
||||
`Invalid extension definition instance, bad version '${internal.version}'`,
|
||||
`Invalid extension definition instance, bad version '${version}'`,
|
||||
);
|
||||
}
|
||||
return internal;
|
||||
@@ -244,7 +275,6 @@ export function createExtension<
|
||||
TInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
never,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
@@ -313,7 +343,13 @@ export function createExtension<
|
||||
>;
|
||||
export function createExtension<
|
||||
UOutput extends AnyExtensionDataRef,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TInputs extends {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
TLegacyInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
|
||||
@@ -330,7 +366,7 @@ export function createExtension<
|
||||
>
|
||||
| LegacyCreateExtensionOptions<
|
||||
AnyExtensionDataMap,
|
||||
TInputs,
|
||||
TLegacyInputs,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema
|
||||
@@ -367,7 +403,7 @@ export function createExtension<
|
||||
|
||||
return {
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
version: Symbol.iterator in options.output ? 'v2' : 'v1',
|
||||
kind: options.kind,
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
@@ -376,16 +412,7 @@ export function createExtension<
|
||||
inputs: options.inputs ?? {},
|
||||
output: options.output,
|
||||
configSchema,
|
||||
factory({ inputs, config, ...rest }) {
|
||||
// TODO: Simplify this, but TS wouldn't infer the input type for some reason
|
||||
return options.factory({
|
||||
inputs: inputs as Expand<ResolvedExtensionInputs<TInputs>>,
|
||||
config: config as TConfig & {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
},
|
||||
factory: options.factory,
|
||||
toString() {
|
||||
const parts: string[] = [];
|
||||
if (options.kind) {
|
||||
|
||||
Reference in New Issue
Block a user