diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index bbf2e539d1..327639af37 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -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' }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index cfce13d494..6727a92419 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -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 }, + TInputs extends { [name in string]: ExtensionInput }, > = { [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] ? Array>> @@ -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 { } /** @internal */ -export interface InternalExtensionDefinition - extends ExtensionDefinition { - readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; - readonly output: AnyExtensionDataMap | Array; - factory(context: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs; - }): ExtensionDataValues | Iterable>; -} +export type InternalExtensionDefinition = + ExtensionDefinition & + ( + | { + readonly version: 'v1'; + readonly inputs: AnyExtensionInputMap; + readonly output: AnyExtensionDataMap; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs; + }): ExtensionDataValues; + } + | { + readonly version: 'v2'; + readonly inputs: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }; + readonly output: Array; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }>; + }): Iterable>; + } + ); /** @internal */ export function toInternalExtensionDefinition( @@ -230,9 +260,10 @@ export function toInternalExtensionDefinition( `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>, - config: config as TConfig & { - [key in keyof TConfigSchema]: z.infer>; - }, - ...rest, - }); - }, + factory: options.factory, toString() { const parts: string[] = []; if (options.kind) {