diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index a8bf4ef17c..101c5cd43a 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -148,7 +148,9 @@ export interface LegacyCreateExtensionOptions< /** @ignore */ export type VerifyExtensionFactoryOutput< UDeclaredOutput extends AnyExtensionDataRef, - UFactoryOutput extends ExtensionDataValue, + UFactoryOutput extends + | ExtensionDataValue + | ExtensionDataContainer, > = ( UDeclaredOutput extends any ? UDeclaredOutput['config']['optional'] extends true @@ -156,21 +158,29 @@ export type VerifyExtensionFactoryOutput< : UDeclaredOutput['id'] : never ) extends infer IRequiredOutputIds - ? [IRequiredOutputIds] extends [UFactoryOutput['id']] - ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] - ? {} + ? ( + UFactoryOutput extends ExtensionDataValue + ? UFactoryOutput['id'] + : UFactoryOutput extends ExtensionDataContainer + ? IDataRefs['id'] + : never + ) extends infer IFactoryOutputIds + ? [IRequiredOutputIds] extends [IFactoryOutputIds] + ? [IFactoryOutputIds] extends [UDeclaredOutput['id']] + ? {} + : { + 'Error: The extension factory has undeclared output(s)': Exclude< + IFactoryOutputIds, + UDeclaredOutput['id'] + >; + } : { - 'Error: The extension factory has undeclared output(s)': Exclude< - UFactoryOutput['id'], - UDeclaredOutput['id'] + 'Error: The extension factory is missing the following output(s)': Exclude< + IRequiredOutputIds, + IFactoryOutputIds >; } - : { - 'Error: The extension factory is missing the following output(s)': Exclude< - IRequiredOutputIds, - UFactoryOutput['id'] - >; - } + : never : never; /** @public */ diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 5f1324fe7c..fed8185df1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -21,6 +21,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; +import { toInternalExtensionDefinition } from './createExtension'; function unused(..._any: any[]) {} @@ -450,4 +451,79 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should replace the outputs when provided through make', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + const ext = toInternalExtensionDefinition( + blueprint.make({ + output: [testDataRef2], + factory(origFactory) { + const parent = origFactory({}); + return [testDataRef2(`${parent.get(testDataRef1)}bar`)]; + }, + }), + ); + + expect(ext.output).toEqual([testDataRef2]); + }); + + it('should allow returning of the parent data container', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield origFactory({}); + yield testDataRef2('bar'); + }, + }); + + expect(true).toBe(true); + // todo: test that the data is actually available + }); + + it('should not allow returning parent output if outputs are overridden', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.make({ + output: [testDataRef2.optional()], + *factory(origFactory) { + // yield testDataRef1('bar'); + yield testDataRef2('bar'); + }, + }); + + expect(true).toBe(true); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 465bb10ee6..e56c30410d 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -204,7 +204,7 @@ class ExtensionBlueprintImpl< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, - UExtraOutput extends AnyExtensionDataRef, + UNewOutput extends AnyExtensionDataRef, TExtraInputs extends { [inputName in string]: ExtensionInput< AnyExtensionDataRef, @@ -220,7 +220,7 @@ class ExtensionBlueprintImpl< [KName in keyof TInputs]?: `Error: Input '${KName & string}' is already defined in parent definition`; }; - output?: Array; + output?: Array; params?: TParams; config?: { schema: TExtensionConfigSchema; @@ -296,7 +296,7 @@ class ExtensionBlueprintImpl< attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: { ...args.inputs, ...this.options.inputs }, - output: [...(args.output ?? []), ...this.options.output], + output: args.output ?? this.options.output, config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { if (args.factory) {