From c33f8df6ebc6516706cb6c7a04b2b33c67836118 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Aug 2024 16:34:52 +0200 Subject: [PATCH 1/9] frontend-plugin-api: initial support for blueprint input overrides Signed-off-by: Patrik Oldsberg --- .../wiring/createExtensionBlueprint.test.tsx | 246 +++++++++++++++++- .../src/wiring/createExtensionBlueprint.ts | 134 +++++++++- 2 files changed, 372 insertions(+), 8 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index b9d1475074..003090668b 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -16,9 +16,15 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; -import { createExtensionBlueprint } from './createExtensionBlueprint'; +import { + createDataContainer, + createExtensionBlueprint, +} from './createExtensionBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { createExtensionDataRef } from './createExtensionDataRef'; +import { + ExtensionDataValue, + createExtensionDataRef, +} from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; import { RouteRef } from '../routing'; import { @@ -28,12 +34,15 @@ import { function unused(..._any: any[]) {} -function factoryOutput(ext: ExtensionDefinition) { +function factoryOutput( + ext: ExtensionDefinition, + inputs: unknown = undefined, +) { const int = toInternalExtensionDefinition(ext); if (int.version !== 'v2') { throw new Error('Expected v2 extension'); } - return Array.from(int.factory({} as any)); + return Array.from(int.factory({ inputs } as any)); } describe('createExtensionBlueprint', () => { @@ -335,6 +344,235 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); + it('should be able to override inputs when calling original factory', () => { + const outputRef = createExtensionDataRef().with({ id: 'output' }); + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const Blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + opt: createExtensionInput([testDataRef1.optional()], { + singleton: true, + optional: true, + }), + single: createExtensionInput([testDataRef1, testDataRef2.optional()], { + singleton: true, + }), + multi: createExtensionInput([testDataRef1]), + }, + output: [outputRef], + factory(_, { inputs }) { + return [ + outputRef({ + opt: inputs.opt?.get(testDataRef1) ?? 'none', + single: inputs.single.get(testDataRef1), + singleOpt: inputs.single.get(testDataRef2) ?? 'none', + multi: inputs.multi.map(i => i.get(testDataRef1)).join(','), + }), + ]; + }, + }); + + const mockInput = (node: string, ...data: ExtensionDataValue[]) => + Object.assign(createDataContainer(data), { + node, + }); + const mockParentInputs = { + opt: mockInput('node-opt', testDataRef1('orig-opt')), + single: mockInput('node-single', testDataRef1('orig-single')), + multi: [ + mockInput('node-multi1', testDataRef1('orig-multi1')), + mockInput('node-multi2', testDataRef1('orig-multi2')), + ], + }; + + // All values provided + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + opt: [testDataRef1('opt')], + single: [testDataRef1('single'), testDataRef2('singleOpt')], + multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'opt', + single: 'single', + singleOpt: 'singleOpt', + multi: 'multi1,multi2', + }), + ]); + + // Minimal values provided + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + single: [testDataRef1('single')], + multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'none', + single: 'single', + singleOpt: 'none', + multi: 'multi1,multi2', + }), + ]); + + // Not enough values provided, checked at runtime + expect(() => + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + single: [], + multi: [], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, + ); + + // Wrong value provided + expect(() => + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory) { + return origFactory( + {}, + { + inputs: { + // @ts-expect-error + opt: [testDataRef2('opt')], + // @ts-expect-error + single: [testDataRef1('single'), outputRef({})], + multi: [ + // @ts-expect-error + [testDataRef2('multi1')], + ], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, + ); + + // Forwarding entire inputs object + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory({}, { inputs }); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }), + ]); + + // Forwarding individual outputs + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + opt: inputs.opt, + single: inputs.single, + multi: inputs.multi, + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }), + ]); + + // Overriding based on original input + expect( + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + single: [ + testDataRef1(`override-${inputs.single.get(testDataRef1)}`), + testDataRef2('new-singleOpt'), + ], + multi: inputs.multi.map(i => [ + testDataRef1(`override-${i.get(testDataRef1)}`), + ]), + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toEqual([ + outputRef({ + opt: 'none', + single: 'override-orig-single', + singleOpt: 'new-singleOpt', + multi: 'override-orig-multi1,override-orig-multi2', + }), + ]); + }); + it('should allow merging of inputs', () => { const blueprint = createExtensionBlueprint({ kind: 'test-extension', diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 656e681a2b..2bc5fda43d 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -20,6 +20,7 @@ import { CreateExtensionOptions, ExtensionDataContainer, ExtensionDefinition, + ResolvedExtensionInput, ResolvedExtensionInputs, VerifyExtensionFactoryOutput, createExtension, @@ -29,6 +30,7 @@ import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, ExtensionDataRef, + ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; @@ -75,6 +77,61 @@ export type CreateExtensionBlueprintOptions< dataRefs?: TDataRefs; } & VerifyExtensionFactoryOutput; +/** @ignore */ +type ResolveInputValueOverrides< + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, +> = Expand< + { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? never + : KName + : never]: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { optional: boolean; singleton: infer ISingleton extends boolean } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } & { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? KName + : never + : never]?: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { optional: boolean; singleton: infer ISingleton extends boolean } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } +>; + /** * @public */ @@ -156,7 +213,7 @@ export interface ExtensionBlueprint< params: TParams, context?: { config?: TConfig; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }, ) => ExtensionDataContainer, context: { @@ -288,7 +345,7 @@ class ExtensionBlueprintImpl< ReturnType >; }; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }, ) => ExtensionDataContainer, context: { @@ -347,14 +404,83 @@ class ExtensionBlueprintImpl< ReturnType >; }; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }, ): ExtensionDataContainer => { + const overrideInputs = innerContext?.inputs; + const declaredInputs = this.options.inputs; + + let forwardedInputs = inputs; + if (overrideInputs && declaredInputs) { + const newInputs = {} as { + [KName in keyof TInputs]?: + | ({ node: AppNode } & ExtensionDataContainer) + | Array<{ node: AppNode } & ExtensionDataContainer>; + }; + forwardedInputs = newInputs as typeof inputs; + + for (const name in declaredInputs) { + if (!Object.hasOwn(declaredInputs, name)) { + continue; + } + const declaredInput = declaredInputs[name]; + const providedData = overrideInputs[name]; + if (declaredInput.config.singleton) { + // We know the passed in input will match the declared inputs, so this case is safe + const originalInput = ( + inputs as Record< + string, + { node: AppNode } & ExtensionDataContainer + > + )[name]; + if (providedData) { + const providedContainer = createDataContainer( + providedData as Iterable>, + ); + if (!originalInput) { + throw new Error( + `A data override was provided for input '${name}', but no original input was present.`, + ); + } + newInputs[name] = Object.assign(providedContainer, { + name: (originalInput as ResolvedExtensionInput).node, + }) as any; + } + } else { + // We know the passed in input will match the declared inputs, so this case is safe + const originalInput = ( + inputs as Record< + string, + Array<{ node: AppNode } & ExtensionDataContainer> + > + )[name]; + if (!Array.isArray(providedData)) { + throw new Error( + `Invalid override provided for input '${name}', expected an array`, + ); + } + if (originalInput.length !== providedData.length) { + throw new Error( + `Invalid override provided for input '${name}', when overriding the input data the length must match the original input data`, + ); + } + newInputs[name] = providedData.map((data, i) => { + const providedContainer = createDataContainer( + data as Iterable>, + ); + return Object.assign(providedContainer, { + name: (originalInput[i] as ResolvedExtensionInput) + .node, + }) as any; + }); + } + } + } 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 + inputs: forwardedInputs as any, // TODO: Might be able to improve this once legacy inputs are gone }), ); }, From eab84be9336594fa19ef1698b7a7f2ed906bce09 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 10 Aug 2024 13:56:03 +0200 Subject: [PATCH 2/9] frontend-plugin-api: validate input override values Signed-off-by: Patrik Oldsberg --- .../wiring/createExtensionBlueprint.test.tsx | 32 ++++++++++++++++--- .../src/wiring/createExtensionBlueprint.ts | 23 +++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 003090668b..fa85cbf981 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -443,17 +443,17 @@ describe('createExtensionBlueprint', () => { }), ]); - // Not enough values provided, checked at runtime + // Mismatched input override length expect(() => factoryOutput( Blueprint.makeWithOverrides({ - factory(origFactory) { + factory(origFactory, { inputs }) { return origFactory( {}, { inputs: { - single: [], - multi: [], + ...inputs, + multi: [[testDataRef1('multi1')]], }, }, ); @@ -465,6 +465,28 @@ describe('createExtensionBlueprint', () => { `"Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, ); + // Required input not provided + expect(() => + factoryOutput( + Blueprint.makeWithOverrides({ + factory(origFactory, { inputs }) { + return origFactory( + {}, + { + inputs: { + ...inputs, + single: [testDataRef2('singleOpt')], + }, + }, + ); + }, + }), + mockParentInputs, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required data values for 'test1'"`, + ); + // Wrong value provided expect(() => factoryOutput( @@ -490,7 +512,7 @@ describe('createExtensionBlueprint', () => { mockParentInputs, ), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, + `"Invalid data value provided, 'test2' was not declared"`, ); // Forwarding entire inputs object diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 2bc5fda43d..2ad58e2ef9 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -259,13 +259,34 @@ export function createDataContainer( ? ExtensionDataValue : never >, + declaredRefs?: ExtensionDataRef[], ): ExtensionDataContainer { const container = new Map>(); + const verifyRefs = + declaredRefs && new Map(declaredRefs.map(ref => [ref.id, ref])); for (const output of values) { + if (verifyRefs) { + if (!verifyRefs.delete(output.id)) { + throw new Error( + `Invalid data value provided, '${output.id}' was not declared`, + ); + } + } container.set(output.id, output); } + const remainingRefs = + verifyRefs && + Array.from(verifyRefs.values()).filter(ref => !ref.config.optional); + if (remainingRefs && remainingRefs.length > 0) { + throw new Error( + `Missing required data values for '${remainingRefs + .map(ref => ref.id) + .join(', ')}'`, + ); + } + return { get(ref) { return container.get(ref.id)?.value; @@ -436,6 +457,7 @@ class ExtensionBlueprintImpl< if (providedData) { const providedContainer = createDataContainer( providedData as Iterable>, + declaredInput.extensionData, ); if (!originalInput) { throw new Error( @@ -467,6 +489,7 @@ class ExtensionBlueprintImpl< newInputs[name] = providedData.map((data, i) => { const providedContainer = createDataContainer( data as Iterable>, + declaredInput.extensionData, ); return Object.assign(providedContainer, { name: (originalInput[i] as ResolvedExtensionInput) From 51b271f4d387cd5e1f72d7d70e110270a91b8ed1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 10 Aug 2024 17:48:36 +0200 Subject: [PATCH 3/9] frontend-plugin-api: validate blueprint original factory output Signed-off-by: Patrik Oldsberg --- .../wiring/createExtensionBlueprint.test.tsx | 37 +++++++++++++++++++ .../src/wiring/createExtensionBlueprint.ts | 1 + 2 files changed, 38 insertions(+) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index fa85cbf981..efca17459b 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -689,6 +689,43 @@ describe('createExtensionBlueprint', () => { expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]); }); + it('should reject invalid output from original factory', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + expect(() => + factoryOutput( + // @ts-expect-error + createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef2('foo')]; + }, + }).makeWithOverrides({ factory: orig => orig({}) }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid data value provided, 'test2' was not declared"`, + ); + + expect(() => + factoryOutput( + // @ts-expect-error + createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return []; + }, + }).makeWithOverrides({ factory: orig => orig({}) }), + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Missing required data values for 'test1'"`, + ); + }); + it('should allow returning of the parent data container', () => { const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 2ad58e2ef9..8ce3dab4d5 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -505,6 +505,7 @@ class ExtensionBlueprintImpl< config: innerContext?.config ?? config, inputs: forwardedInputs as any, // TODO: Might be able to improve this once legacy inputs are gone }), + this.options.output, ); }, { From 9f476d1687e805cd3a5edec426c25bf51c5b9217 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Aug 2024 12:54:01 +0200 Subject: [PATCH 4/9] frontend-plugin-api: refactor out blueprint input overrides Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtensionBlueprint.ts | 152 +++++++++--------- 1 file changed, 80 insertions(+), 72 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 8ce3dab4d5..88f3a6e9dd 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -297,6 +297,81 @@ export function createDataContainer( } as ExtensionDataContainer; } +function expectArray(value: T | T[]): T[] { + return value as T[]; +} +function expectItem(value: T | T[]): T { + return value as T; +} + +/** @internal */ +export function resolveInputOverrides( + declaredInputs?: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + inputs?: { + [KName in string]?: + | ({ node: AppNode } & ExtensionDataContainer) + | Array<{ node: AppNode } & ExtensionDataContainer>; + }, + inputOverrides?: ResolveInputValueOverrides, +) { + if (!declaredInputs || !inputs || !inputOverrides) { + return inputs; + } + + const newInputs: typeof inputs = {}; + for (const name in declaredInputs) { + if (!Object.hasOwn(declaredInputs, name)) { + continue; + } + const declaredInput = declaredInputs[name]; + const providedData = inputOverrides[name]; + if (declaredInput.config.singleton) { + const originalInput = expectItem(inputs[name]); + if (providedData) { + const providedContainer = createDataContainer( + providedData as Iterable>, + declaredInput.extensionData, + ); + if (!originalInput) { + throw new Error( + `A data override was provided for input '${name}', but no original input was present.`, + ); + } + newInputs[name] = Object.assign(providedContainer, { + name: (originalInput as ResolvedExtensionInput).node, + }) as any; + } + } else { + const originalInput = expectArray(inputs[name]); + if (!Array.isArray(providedData)) { + throw new Error( + `Invalid override provided for input '${name}', expected an array`, + ); + } + if (originalInput.length !== providedData.length) { + throw new Error( + `Invalid override provided for input '${name}', when overriding the input data the length must match the original input data`, + ); + } + newInputs[name] = providedData.map((data, i) => { + const providedContainer = createDataContainer( + data as Iterable>, + declaredInput.extensionData, + ); + return Object.assign(providedContainer, { + name: (originalInput[i] as ResolvedExtensionInput).node, + }) as any; + }); + } + } + return newInputs; +} + /** * @internal */ @@ -428,82 +503,15 @@ class ExtensionBlueprintImpl< inputs?: ResolveInputValueOverrides; }, ): ExtensionDataContainer => { - const overrideInputs = innerContext?.inputs; - const declaredInputs = this.options.inputs; - - let forwardedInputs = inputs; - if (overrideInputs && declaredInputs) { - const newInputs = {} as { - [KName in keyof TInputs]?: - | ({ node: AppNode } & ExtensionDataContainer) - | Array<{ node: AppNode } & ExtensionDataContainer>; - }; - forwardedInputs = newInputs as typeof inputs; - - for (const name in declaredInputs) { - if (!Object.hasOwn(declaredInputs, name)) { - continue; - } - const declaredInput = declaredInputs[name]; - const providedData = overrideInputs[name]; - if (declaredInput.config.singleton) { - // We know the passed in input will match the declared inputs, so this case is safe - const originalInput = ( - inputs as Record< - string, - { node: AppNode } & ExtensionDataContainer - > - )[name]; - if (providedData) { - const providedContainer = createDataContainer( - providedData as Iterable>, - declaredInput.extensionData, - ); - if (!originalInput) { - throw new Error( - `A data override was provided for input '${name}', but no original input was present.`, - ); - } - newInputs[name] = Object.assign(providedContainer, { - name: (originalInput as ResolvedExtensionInput).node, - }) as any; - } - } else { - // We know the passed in input will match the declared inputs, so this case is safe - const originalInput = ( - inputs as Record< - string, - Array<{ node: AppNode } & ExtensionDataContainer> - > - )[name]; - if (!Array.isArray(providedData)) { - throw new Error( - `Invalid override provided for input '${name}', expected an array`, - ); - } - if (originalInput.length !== providedData.length) { - throw new Error( - `Invalid override provided for input '${name}', when overriding the input data the length must match the original input data`, - ); - } - newInputs[name] = providedData.map((data, i) => { - const providedContainer = createDataContainer( - data as Iterable>, - declaredInput.extensionData, - ); - return Object.assign(providedContainer, { - name: (originalInput[i] as ResolvedExtensionInput) - .node, - }) as any; - }); - } - } - } return createDataContainer( this.options.factory(innerParams, { node, config: innerContext?.config ?? config, - inputs: forwardedInputs as any, // TODO: Might be able to improve this once legacy inputs are gone + inputs: resolveInputOverrides( + this.options.inputs, + inputs, + innerContext?.inputs, + ) as any, // TODO: Might be able to improve this once legacy inputs are gone }), this.options.output, ); From 66c6bbcef5d0fe0b904f5841141ce50a65d1961b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Aug 2024 19:57:06 +0200 Subject: [PATCH 5/9] frontend-app-api: fix for input extension data containers not being iterable at runtime Signed-off-by: Patrik Oldsberg --- .../src/tree/instantiateAppNodeTree.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 65626e0be1..c70e351105 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -92,6 +92,16 @@ function resolveInputDataContainer( get(ref) { return dataMap.get(ref.id); }, + *[Symbol.iterator]() { + for (const [id, value] of dataMap) { + // TODO: Would be better to be able to create a new instance using the ref here instead + yield { + $$type: '@backstage/ExtensionDataValue', + id, + value, + }; + } + }, } as { node: AppNode } & ExtensionDataContainer; } From cea81165f45f3cb9c6488ce66f18cda7d07c5c91 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Aug 2024 19:57:42 +0200 Subject: [PATCH 6/9] frontend-plugin-api: add support for overriding input values when overriding extensions Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.test.ts | 292 ++++++++++++++++++ .../src/wiring/createExtension.ts | 19 +- .../src/wiring/createExtensionBlueprint.ts | 2 +- 3 files changed, 307 insertions(+), 6 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index f9e39e98de..9d2aff5d99 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -732,5 +732,297 @@ describe('createExtension', () => { }).data(stringDataRef), ).toBe('foo-hello-override-world'); }); + + it('should be able to override input values', () => { + const outputRef = createExtensionDataRef().with({ + id: 'output', + }); + const testDataRef1 = createExtensionDataRef().with({ + id: 'test1', + }); + const testDataRef2 = createExtensionDataRef().with({ + id: 'test2', + }); + + const subject = createExtension({ + name: 'subject', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + opt: createExtensionInput([testDataRef1.optional()], { + singleton: true, + optional: true, + }), + single: createExtensionInput( + [testDataRef1, testDataRef2.optional()], + { + singleton: true, + }, + ), + multi: createExtensionInput([testDataRef1]), + }, + output: [outputRef], + factory({ inputs }) { + return [ + outputRef({ + opt: inputs.opt?.get(testDataRef1) ?? 'none', + single: inputs.single.get(testDataRef1), + singleOpt: inputs.single.get(testDataRef2) ?? 'none', + multi: inputs.multi.map(i => i.get(testDataRef1)).join(','), + }), + ]; + }, + }); + + const optExt = createExtension({ + name: 'opt', + attachTo: { id: 'subject', input: 'opt' }, + output: [testDataRef1], + factory: () => [testDataRef1('orig-opt')], + }); + + const singleExt = createExtension({ + name: 'single', + attachTo: { id: 'subject', input: 'single' }, + output: [testDataRef1, testDataRef2.optional()], + factory: () => [testDataRef1('orig-single')], + }); + + const multi1Ext = createExtension({ + name: 'multi1', + attachTo: { id: 'subject', input: 'multi' }, + output: [testDataRef1], + factory: () => [testDataRef1('orig-multi1')], + }); + + const multi2Ext = createExtension({ + name: 'multi2', + attachTo: { id: 'subject', input: 'multi' }, + output: [testDataRef1], + factory: () => [testDataRef1('orig-multi2')], + }); + + expect( + createExtensionTester(subject) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }); + + // All values provided + expect( + createExtensionTester( + subject.override({ + factory(originalFactory) { + return originalFactory({ + inputs: { + opt: [testDataRef1('opt')], + single: [testDataRef1('single'), testDataRef2('singleOpt')], + multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toEqual({ + opt: 'opt', + single: 'single', + singleOpt: 'singleOpt', + multi: 'multi1,multi2', + }); + + // Minimal values provided + expect( + createExtensionTester( + subject.override({ + factory(originalFactory) { + return originalFactory({ + inputs: { + single: [testDataRef1('single')], + multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toEqual({ + opt: 'none', + single: 'single', + singleOpt: 'none', + multi: 'multi1,multi2', + }); + + // Forward inputs directly + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }); + + // Forward inputs separately + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + opt: inputs.opt, + single: inputs.single, + multi: inputs.multi, + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toEqual({ + opt: 'orig-opt', + single: 'orig-single', + singleOpt: 'none', + multi: 'orig-multi1,orig-multi2', + }); + + // Overriding based on original input + expect( + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + single: [ + testDataRef1(`override-${inputs.single.get(testDataRef1)}`), + testDataRef2('new-singleOpt'), + ], + multi: inputs.multi.map(i => [ + testDataRef1(`override-${i.get(testDataRef1)}`), + ]), + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toEqual({ + opt: 'none', + single: 'override-orig-single', + singleOpt: 'new-singleOpt', + multi: 'override-orig-multi1,override-orig-multi2', + }); + + // Mismatched input override length + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + ...inputs, + multi: [[testDataRef1('multi1')]], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, + ); + + // Required input not provided + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory, { inputs }) { + return originalFactory({ + inputs: { + ...inputs, + single: [testDataRef2('singleOpt')], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', Missing required data values for 'test1'"`, + ); + + // Wrong value provided + expect(() => + createExtensionTester( + subject.override({ + factory(originalFactory) { + return originalFactory({ + inputs: { + // @ts-expect-error + opt: [testDataRef2('opt')], + // @ts-expect-error + single: [testDataRef1('single'), outputRef({})], + multi: [ + // @ts-expect-error + [testDataRef2('multi1')], + ], + }, + }); + }, + }), + ) + .add(optExt) + .add(singleExt) + .add(multi1Ext) + .add(multi2Ext) + .data(outputRef), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'subject', Invalid data value provided, 'test2' was not declared"`, + ); + }); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index f2d3d906ab..55d71e0d2f 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -17,7 +17,11 @@ import { AppNode } from '../apis'; import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; -import { createDataContainer } from './createExtensionBlueprint'; +import { + ResolveInputValueOverrides, + createDataContainer, + resolveInputOverrides, +} from './createExtensionBlueprint'; import { AnyExtensionDataRef, ExtensionDataRef, @@ -259,7 +263,7 @@ export interface ExtensionDefinition< factory( originalFactory: (context?: { config?: TConfig; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }) => ExtensionDataContainer, context: { node: AppNode; @@ -558,7 +562,7 @@ export function createExtension< ReturnType >; }; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }) => ExtensionDataContainer, context: { node: AppNode; @@ -646,14 +650,19 @@ export function createExtension< ReturnType >; }; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }): ExtensionDataContainer => { return createDataContainer( newOptions.factory({ node, config: innerContext?.config ?? config, - inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + inputs: resolveInputOverrides( + newOptions.inputs, + inputs, + innerContext?.inputs, + ) as any, // TODO: Might be able to improve this once legacy inputs are gone }) as Iterable, + newOptions.output, ); }, { diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 88f3a6e9dd..cd6c0d6968 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -78,7 +78,7 @@ export type CreateExtensionBlueprintOptions< } & VerifyExtensionFactoryOutput; /** @ignore */ -type ResolveInputValueOverrides< +export type ResolveInputValueOverrides< TInputs extends { [inputName in string]: ExtensionInput< AnyExtensionDataRef, From 3d7d38d94f3ef904062461ebbf9ef1c638d8a0ad Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Aug 2024 20:04:48 +0200 Subject: [PATCH 7/9] frontend-plugin-api: clean up error messages for input overrides Signed-off-by: Patrik Oldsberg --- .../src/wiring/createExtension.test.ts | 6 +++--- .../src/wiring/createExtensionBlueprint.test.tsx | 10 +++++----- .../src/wiring/createExtensionBlueprint.ts | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 9d2aff5d99..d6e2599138 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -969,7 +969,7 @@ describe('createExtension', () => { .add(multi2Ext) .data(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, + `"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, ); // Required input not provided @@ -992,7 +992,7 @@ describe('createExtension', () => { .add(multi2Ext) .data(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', Missing required data values for 'test1'"`, + `"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, ); // Wrong value provided @@ -1021,7 +1021,7 @@ describe('createExtension', () => { .add(multi2Ext) .data(outputRef), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'subject', Invalid data value provided, 'test2' was not declared"`, + `"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, ); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index efca17459b..508000acbb 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -462,7 +462,7 @@ describe('createExtensionBlueprint', () => { mockParentInputs, ), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid override provided for input 'multi', when overriding the input data the length must match the original input data"`, + `"override data provided for input 'multi' must match the length of the original inputs"`, ); // Required input not provided @@ -484,7 +484,7 @@ describe('createExtensionBlueprint', () => { mockParentInputs, ), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required data values for 'test1'"`, + `"missing required extension data value(s) 'test1'"`, ); // Wrong value provided @@ -512,7 +512,7 @@ describe('createExtensionBlueprint', () => { mockParentInputs, ), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid data value provided, 'test2' was not declared"`, + `"extension data 'test2' was provided but not declared"`, ); // Forwarding entire inputs object @@ -706,7 +706,7 @@ describe('createExtensionBlueprint', () => { }).makeWithOverrides({ factory: orig => orig({}) }), ), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid data value provided, 'test2' was not declared"`, + `"extension data 'test2' was provided but not declared"`, ); expect(() => @@ -722,7 +722,7 @@ describe('createExtensionBlueprint', () => { }).makeWithOverrides({ factory: orig => orig({}) }), ), ).toThrowErrorMatchingInlineSnapshot( - `"Missing required data values for 'test1'"`, + `"missing required extension data value(s) 'test1'"`, ); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index cd6c0d6968..74d4426207 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -269,7 +269,7 @@ export function createDataContainer( if (verifyRefs) { if (!verifyRefs.delete(output.id)) { throw new Error( - `Invalid data value provided, '${output.id}' was not declared`, + `extension data '${output.id}' was provided but not declared`, ); } } @@ -281,7 +281,7 @@ export function createDataContainer( Array.from(verifyRefs.values()).filter(ref => !ref.config.optional); if (remainingRefs && remainingRefs.length > 0) { throw new Error( - `Missing required data values for '${remainingRefs + `missing required extension data value(s) '${remainingRefs .map(ref => ref.id) .join(', ')}'`, ); @@ -339,7 +339,7 @@ export function resolveInputOverrides( ); if (!originalInput) { throw new Error( - `A data override was provided for input '${name}', but no original input was present.`, + `attempted to override data of input '${name}' but it is not present in the original inputs`, ); } newInputs[name] = Object.assign(providedContainer, { @@ -350,12 +350,12 @@ export function resolveInputOverrides( const originalInput = expectArray(inputs[name]); if (!Array.isArray(providedData)) { throw new Error( - `Invalid override provided for input '${name}', expected an array`, + `override data provided for input '${name}' must be an array`, ); } if (originalInput.length !== providedData.length) { throw new Error( - `Invalid override provided for input '${name}', when overriding the input data the length must match the original input data`, + `override data provided for input '${name}' must match the length of the original inputs`, ); } newInputs[name] = providedData.map((data, i) => { From cf3de79c245b42706b7741a24572096e25d2f747 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 12 Aug 2024 09:52:35 +0200 Subject: [PATCH 8/9] frontend-plugin-api: allow clearing multi inputs Signed-off-by: Patrik Oldsberg --- .../frontend-plugin-api/src/wiring/createExtension.test.ts | 4 ++-- .../src/wiring/createExtensionBlueprint.test.tsx | 4 ++-- .../src/wiring/createExtensionBlueprint.ts | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index d6e2599138..014ccf4a40 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -850,7 +850,7 @@ describe('createExtension', () => { return originalFactory({ inputs: { single: [testDataRef1('single')], - multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + multi: [], }, }); }, @@ -865,7 +865,7 @@ describe('createExtension', () => { opt: 'none', single: 'single', singleOpt: 'none', - multi: 'multi1,multi2', + multi: '', }); // Forward inputs directly diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 508000acbb..57b0629859 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -426,7 +426,7 @@ describe('createExtensionBlueprint', () => { { inputs: { single: [testDataRef1('single')], - multi: [[testDataRef1('multi1')], [testDataRef1('multi2')]], + multi: [], }, }, ); @@ -439,7 +439,7 @@ describe('createExtensionBlueprint', () => { opt: 'none', single: 'single', singleOpt: 'none', - multi: 'multi1,multi2', + multi: '', }), ]); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 74d4426207..8175a25212 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -353,7 +353,10 @@ export function resolveInputOverrides( `override data provided for input '${name}' must be an array`, ); } - if (originalInput.length !== providedData.length) { + if ( + originalInput.length !== providedData.length && + providedData.length > 0 + ) { throw new Error( `override data provided for input '${name}' must match the length of the original inputs`, ); From fb317712fa0e19e17a43a50ebfbbe7c8e09ef9a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 12 Aug 2024 10:09:10 +0200 Subject: [PATCH 9/9] frontend-plugin-api: fix and update API report Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/api-report.md | 71 ++++++++++++++++++- .../src/wiring/createExtensionBlueprint.ts | 2 +- .../frontend-plugin-api/src/wiring/index.ts | 1 + 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index f39add924a..6d548e3c26 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -1281,7 +1281,7 @@ export interface ExtensionBlueprint< params: TParams, context?: { config?: TConfig; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }, ) => ExtensionDataContainer, context: { @@ -1470,7 +1470,7 @@ export interface ExtensionDefinition< factory( originalFactory: (context?: { config?: TConfig; - inputs?: Expand>; + inputs?: ResolveInputValueOverrides; }) => ExtensionDataContainer, context: { node: AppNode; @@ -1868,6 +1868,73 @@ export type ResolvedExtensionInputs< : Expand | undefined>; }; +// @public (undocumented) +export type ResolveInputValueOverrides< + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + } = { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, +> = Expand< + { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? never + : KName + : never]: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { + optional: boolean; + singleton: infer ISingleton extends boolean; + } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } & { + [KName in keyof TInputs as TInputs[KName] extends ExtensionInput< + any, + { + optional: infer IOptional extends boolean; + singleton: boolean; + } + > + ? IOptional extends true + ? KName + : never + : never]?: TInputs[KName] extends ExtensionInput< + infer IDataRefs, + { + optional: boolean; + singleton: infer ISingleton extends boolean; + } + > + ? ISingleton extends true + ? Iterable> + : Array>> + : never; + } +>; + // @public export type RouteFunc = ( ...[params]: TParams extends undefined diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 8175a25212..01f1fa09aa 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -77,7 +77,7 @@ export type CreateExtensionBlueprintOptions< dataRefs?: TDataRefs; } & VerifyExtensionFactoryOutput; -/** @ignore */ +/** @public */ export type ResolveInputValueOverrides< TInputs extends { [inputName in string]: ExtensionInput< diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index a17dfedc16..a28069da88 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -56,6 +56,7 @@ export { } from './types'; export { type CreateExtensionBlueprintOptions, + type ResolveInputValueOverrides, type ExtensionBlueprint, createExtensionBlueprint, } from './createExtensionBlueprint';