From fed70efe37f27ce7f589108475c3be29aa4d69d0 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 16:03:35 +0200 Subject: [PATCH 1/9] chore: working through the inputs merging Signed-off-by: blam --- .../wiring/createExtensionBlueprint.test.tsx | 66 +++++++++++++++++++ .../src/wiring/createExtensionBlueprint.ts | 39 +++++++---- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index dfdbfface6..5f1324fe7c 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -384,4 +384,70 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + + it('should allow merging of inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement.optional()], + factory(_params: { x?: string }, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + unused(ref); + return []; + }, + }); + + blueprint.make({ + inputs: { + test2: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + factory(origFactory, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + const el: JSX.Element = inputs.test2.get( + coreExtensionData.reactElement, + ); + + unused(ref, el); + + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); + + it('should not allow overriding inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef]), + }, + output: [coreExtensionData.reactElement.optional()], + factory() { + return []; + }, + }); + + blueprint.make({ + inputs: { + // @ts-expect-error + test: createExtensionInput([]), // Overrides are not allowed + }, + factory(origFactory) { + return origFactory({}); + }, + }); + + 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 46db278bf8..90736703a0 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -83,7 +83,6 @@ export interface ExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown }, TConfigInput extends { [key in string]: unknown }, TDataRefs extends { [name in string]: AnyExtensionDataRef }, @@ -101,13 +100,23 @@ export interface ExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UExtraOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, >( args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TInputs; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; output?: Array; config?: { schema: TExtensionConfigSchema & { @@ -132,7 +141,7 @@ export interface ExtensionBlueprint< ReturnType >; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; } & VerifyExtensionFactoryOutput< @@ -172,7 +181,6 @@ class ExtensionBlueprintImpl< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, TDataRefs extends { [name in string]: AnyExtensionDataRef }, > { @@ -196,12 +204,22 @@ class ExtensionBlueprintImpl< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UExtraOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, >(args: { namespace?: string; name?: string; attachTo?: { id: string; input: string }; disabled?: boolean; - inputs?: TInputs; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; output?: Array; params?: TParams; config?: { @@ -228,7 +246,7 @@ class ExtensionBlueprintImpl< } & { [key in keyof TConfigSchema]: z.infer>; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; }): ExtensionDefinition< @@ -277,7 +295,7 @@ class ExtensionBlueprintImpl< name: args.name ?? name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, - inputs: args.inputs ?? this.options.inputs, + inputs: { ...args.inputs, ...this.options.inputs }, output: [...(args.output ?? []), ...this.options.output], config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { @@ -297,7 +315,7 @@ class ExtensionBlueprintImpl< return this.options.factory(innerParams, { node, config: innerContext?.config ?? config, - inputs: innerContext?.inputs ?? inputs, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden }); }, { @@ -317,7 +335,7 @@ class ExtensionBlueprintImpl< }, } as CreateExtensionOptions< UOutput, - TInputs, + TInputs & TExtraInputs, { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -357,7 +375,6 @@ export function createExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, UFactoryOutput extends ExtensionDataValue, TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, @@ -374,7 +391,6 @@ export function createExtensionBlueprint< TParams, UOutput, TInputs, - UExtraOutput, string extends keyof TConfigSchema ? {} : { [key in keyof TConfigSchema]: z.infer> }, @@ -391,7 +407,6 @@ export function createExtensionBlueprint< TParams, UOutput, TInputs, - UExtraOutput, string extends keyof TConfigSchema ? {} : { From 65a1f99d0320179a22129956d5f6291f1765d73a Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 17:11:56 +0200 Subject: [PATCH 2/9] chore: Fixing some typescript errors Signed-off-by: blam --- .../src/wiring/createExtensionBlueprint.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 90736703a0..465bb10ee6 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -328,7 +328,10 @@ class ExtensionBlueprintImpl< return this.options.factory(args.params, { node, config, - inputs, + // TODO: Figure out types once legacy data map input type is gone + inputs: inputs as unknown as Expand< + ResolvedExtensionInputs + >, }); } throw new Error('Either params or factory must be provided'); From 34f1b2aeb1234428fe74e4581161845dd8fbfdff Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 17:13:50 +0200 Subject: [PATCH 3/9] chore: changeset Signed-off-by: blam --- .changeset/six-mails-smell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-mails-smell.md diff --git a/.changeset/six-mails-smell.md b/.changeset/six-mails-smell.md new file mode 100644 index 0000000000..db39259632 --- /dev/null +++ b/.changeset/six-mails-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Support merging of `inputs` in `Blueprints` From 1533b959ef56550d5dfa38ecd4591845796522b1 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 5 Aug 2024 17:35:13 +0200 Subject: [PATCH 4/9] chore: steps to making outputs and factories look pretty Signed-off-by: blam --- .../src/wiring/createExtension.ts | 36 +++++---- .../wiring/createExtensionBlueprint.test.tsx | 76 +++++++++++++++++++ .../src/wiring/createExtensionBlueprint.ts | 6 +- 3 files changed, 102 insertions(+), 16 deletions(-) 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) { From eda2165093264814f648d0e9db7f495149701a79 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 15:47:57 +0200 Subject: [PATCH 5/9] frontend-plugin-api: working types for extension data containers as iterables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.ts | 12 +++++++++++- .../src/wiring/createExtensionBlueprint.test.tsx | 12 +++++++++--- .../src/wiring/createExtensionBlueprint.ts | 14 +++++++------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 101c5cd43a..2c82c44ee1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -65,7 +65,17 @@ export type ExtensionDataValues = { /** @public */ export type ExtensionDataContainer = - { + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { get( ref: ExtensionDataRef, ): UExtensionData extends ExtensionDataRef diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index fed8185df1..70d874d9ad 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -494,11 +494,18 @@ describe('createExtensionBlueprint', () => { blueprint.make({ output: [testDataRef1, testDataRef2], *factory(origFactory) { - yield origFactory({}); + yield* origFactory({}); yield testDataRef2('bar'); }, }); + blueprint.make({ + output: [testDataRef1, testDataRef2], + factory(origFactory) { + return [...origFactory({}), testDataRef2('bar')]; + }, + }); + expect(true).toBe(true); // todo: test that the data is actually available }); @@ -518,8 +525,7 @@ describe('createExtensionBlueprint', () => { blueprint.make({ output: [testDataRef2.optional()], - *factory(origFactory) { - // yield testDataRef1('bar'); + *factory() { yield testDataRef2('bar'); }, }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index e56c30410d..603ffda621 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -18,6 +18,7 @@ import { AppNode } from '../apis'; import { Expand } from '../types'; import { CreateExtensionOptions, + ExtensionDataContainer, ExtensionDefinition, ResolvedExtensionInputs, VerifyExtensionFactoryOutput, @@ -27,7 +28,6 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, - ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; @@ -100,7 +100,7 @@ export interface ExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, - UExtraOutput extends AnyExtensionDataRef, + UNewOutput extends AnyExtensionDataRef, TExtraInputs extends { [inputName in string]: ExtensionInput< AnyExtensionDataRef, @@ -117,7 +117,7 @@ export interface ExtensionBlueprint< [KName in keyof TInputs]?: `Error: Input '${KName & string}' is already defined in parent definition`; }; - output?: Array; + output?: Array; config?: { schema: TExtensionConfigSchema & { [KName in keyof TConfig]?: `Error: Config key '${KName & @@ -133,7 +133,7 @@ export interface ExtensionBlueprint< config?: TConfig; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: TConfig & { @@ -145,7 +145,7 @@ export interface ExtensionBlueprint< }, ): Iterable; } & VerifyExtensionFactoryOutput< - UOutput & UExtraOutput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, UFactoryOutput >) | { @@ -236,7 +236,7 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: { @@ -311,7 +311,7 @@ class ExtensionBlueprintImpl< }; inputs?: Expand>; }, - ): Iterable> => { + ): ExtensionDataContainer => { return this.options.factory(innerParams, { node, config: innerContext?.config ?? config, From e8867e17f1fb751cb47b85d273fb93a2ba4e3bc3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:23:26 +0200 Subject: [PATCH 6/9] frontend-plugin-api: extension data container runtime + tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../wiring/createExtensionBlueprint.test.tsx | 99 +++++++++++++++---- .../src/wiring/createExtensionBlueprint.ts | 37 ++++++- 2 files changed, 113 insertions(+), 23 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 70d874d9ad..66c3939a96 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -21,10 +21,21 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; -import { toInternalExtensionDefinition } from './createExtension'; +import { + ExtensionDefinition, + toInternalExtensionDefinition, +} from './createExtension'; function unused(..._any: any[]) {} +function factoryOutput(ext: ExtensionDefinition) { + const int = toInternalExtensionDefinition(ext); + if (int.version !== 'v2') { + throw new Error('Expected v2 extension'); + } + return Array.from(int.factory({} as any)); +} + describe('createExtensionBlueprint', () => { it('should allow creation of extension blueprints', () => { const TestExtensionBlueprint = createExtensionBlueprint({ @@ -476,6 +487,8 @@ describe('createExtensionBlueprint', () => { ); expect(ext.output).toEqual([testDataRef2]); + + expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]); }); it('should allow returning of the parent data container', () => { @@ -491,23 +504,28 @@ describe('createExtensionBlueprint', () => { }, }); - blueprint.make({ - output: [testDataRef1, testDataRef2], - *factory(origFactory) { - yield* origFactory({}); - yield testDataRef2('bar'); - }, - }); + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); - blueprint.make({ - output: [testDataRef1, testDataRef2], - factory(origFactory) { - return [...origFactory({}), testDataRef2('bar')]; - }, - }); - - expect(true).toBe(true); - // todo: test that the data is actually available + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef1, testDataRef2], + factory(origFactory) { + return [...origFactory({}), testDataRef2('bar')]; + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); }); it('should not allow returning parent output if outputs are overridden', () => { @@ -523,13 +541,58 @@ describe('createExtensionBlueprint', () => { }, }); + // @ts-expect-error blueprint.make({ output: [testDataRef2.optional()], *factory() { + yield testDataRef1('foo'); yield testDataRef2('bar'); }, }); - expect(true).toBe(true); + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef2.optional()], + *factory() { + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef2('bar')]); + }); + + it('should not rely on optional outputs when forwarding from parent', () => { + 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, testDataRef2.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + // @ts-expect-error + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + }, + }); + + expect( + factoryOutput( + blueprint.make({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 603ffda621..2b80dac81e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -28,6 +28,7 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, + ExtensionDataRef, ExtensionDataValue, } from './createExtensionDataRef'; @@ -169,6 +170,30 @@ export interface ExtensionBlueprint< >; } +/** @internal */ +function createDataContainer( + values: Iterable< + UData extends ExtensionDataRef + ? ExtensionDataValue + : never + >, +): ExtensionDataContainer { + const container = new Map>(); + + for (const output of values) { + container.set(output.id, output); + } + + return { + get(ref) { + return container.get(ref.id)?.value; + }, + [Symbol.iterator]() { + return container.values(); + }, + } as ExtensionDataContainer; +} + /** * @internal */ @@ -312,11 +337,13 @@ class ExtensionBlueprintImpl< inputs?: Expand>; }, ): ExtensionDataContainer => { - return this.options.factory(innerParams, { - node, - config: innerContext?.config ?? config, - inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden - }); + return createDataContainer( + this.options.factory(innerParams, { + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }), + ); }, { node, From 8015a2d81c730c28be30598cc1cb9cd5a9fa5dcc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:27:18 +0200 Subject: [PATCH 7/9] changesets: update blueprints changeset Signed-off-by: Patrik Oldsberg --- .changeset/six-mails-smell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/six-mails-smell.md b/.changeset/six-mails-smell.md index db39259632..3845c72626 100644 --- a/.changeset/six-mails-smell.md +++ b/.changeset/six-mails-smell.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': patch --- -Support merging of `inputs` in `Blueprints` +Support merging of `inputs` in extension blueprints, but stop merging `output`. In addition, the original factory in extension blueprints now returns a data container that both provides access to the returned data, but can also be forwarded as output. From 51a4ef638b0406c7bbb1ade7a267ae33af3d91b9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:29:14 +0200 Subject: [PATCH 8/9] frontend-plugin-api: update API report Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/api-report.md | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index cbe92463ba..38b980fcbf 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -603,7 +603,6 @@ export function createExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -624,7 +623,6 @@ export function createExtensionBlueprint< TParams, UOutput, TInputs, - UExtraOutput, string extends keyof TConfigSchema ? {} : { @@ -1090,7 +1088,6 @@ export interface ExtensionBlueprint< } >; }, - UExtraOutput extends AnyExtensionDataRef, TConfig extends { [key in string]: unknown; }, @@ -1108,6 +1105,16 @@ export interface ExtensionBlueprint< [key in string]: (zImpl: typeof z) => z.ZodType; }, UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, >( args: { namespace?: string; @@ -1117,8 +1124,11 @@ export interface ExtensionBlueprint< input: string; }; disabled?: boolean; - inputs?: TInputs; - output?: Array; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; config?: { schema: TExtensionConfigSchema & { [KName in keyof TConfig]?: `Error: Config key '${KName & @@ -1134,7 +1144,7 @@ export interface ExtensionBlueprint< config?: TConfig; inputs?: Expand>; }, - ) => Iterable>, + ) => ExtensionDataContainer, context: { node: AppNode; config: TConfig & { @@ -1142,11 +1152,11 @@ export interface ExtensionBlueprint< ReturnType >; }; - inputs: Expand>; + inputs: Expand>; }, ): Iterable; } & VerifyExtensionFactoryOutput< - UOutput & UExtraOutput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, UFactoryOutput >) | { @@ -1186,7 +1196,17 @@ export interface ExtensionBoundaryProps { // @public (undocumented) export type ExtensionDataContainer = - { + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { get( ref: ExtensionDataRef, ): UExtensionData extends ExtensionDataRef @@ -1356,7 +1376,6 @@ export const IconBundleBlueprint: ExtensionBlueprint< } >; }, - AnyExtensionDataRef, { icons: string; test: string; From 2212c3ef08c63cd54ef19333200c63da9e7d895f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 6 Aug 2024 17:32:28 +0200 Subject: [PATCH 9/9] frontend-plugin-api: clean up output verification Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.ts | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index 2c82c44ee1..2a29fe1ab4 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -158,9 +158,7 @@ export interface LegacyCreateExtensionOptions< /** @ignore */ export type VerifyExtensionFactoryOutput< UDeclaredOutput extends AnyExtensionDataRef, - UFactoryOutput extends - | ExtensionDataValue - | ExtensionDataContainer, + UFactoryOutput extends ExtensionDataValue, > = ( UDeclaredOutput extends any ? UDeclaredOutput['config']['optional'] extends true @@ -168,29 +166,21 @@ export type VerifyExtensionFactoryOutput< : UDeclaredOutput['id'] : never ) extends infer IRequiredOutputIds - ? ( - 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'] - >; - } + ? [IRequiredOutputIds] extends [UFactoryOutput['id']] + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} : { - 'Error: The extension factory is missing the following output(s)': Exclude< - IRequiredOutputIds, - IFactoryOutputIds + 'Error: The extension factory has undeclared output(s)': Exclude< + UFactoryOutput['id'], + UDeclaredOutput['id'] >; } - : never + : { + 'Error: The extension factory is missing the following output(s)': Exclude< + IRequiredOutputIds, + UFactoryOutput['id'] + >; + } : never; /** @public */