diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index a3be782fc8..86fcdc56ee 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -19,12 +19,14 @@ import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; import { ResolveInputValueOverrides, - createDataContainer, resolveInputOverrides, -} from './createExtensionBlueprint'; +} from './resolveInputOverrides'; +import { + ExtensionDataContainer, + createExtensionDataContainer, +} from './createExtensionDataContainer'; import { AnyExtensionDataRef, - ExtensionDataRef, ExtensionDataValue, } from './createExtensionDataRef'; import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; @@ -68,28 +70,6 @@ export type ExtensionDataValues = { : never]?: TExtensionData[DataName]['T']; }; -/** @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 - ? IConfig['optional'] extends true - ? IData | undefined - : IData - : never; - }; - /** * Convert a single extension input into a matching resolved input. * @public @@ -669,7 +649,7 @@ export function createExtension< }; inputs?: ResolveInputValueOverrides; }): ExtensionDataContainer => { - return createDataContainer( + return createExtensionDataContainer( newOptions.factory({ node, config: innerContext?.config ?? config, diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 57b0629859..f2f17079ca 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -16,10 +16,7 @@ import React from 'react'; import { coreExtensionData } from './coreExtensionData'; -import { - createDataContainer, - createExtensionBlueprint, -} from './createExtensionBlueprint'; +import { createExtensionBlueprint } from './createExtensionBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; import { ExtensionDataValue, @@ -31,6 +28,7 @@ import { ExtensionDefinition, toInternalExtensionDefinition, } from './createExtension'; +import { createExtensionDataContainer } from './createExtensionDataContainer'; function unused(..._any: any[]) {} @@ -376,7 +374,7 @@ describe('createExtensionBlueprint', () => { }); const mockInput = (node: string, ...data: ExtensionDataValue[]) => - Object.assign(createDataContainer(data), { + Object.assign(createExtensionDataContainer(data), { node, }); const mockParentInputs = { diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 01f1fa09aa..d1fc75bca6 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -18,9 +18,7 @@ import { AppNode } from '../apis'; import { Expand } from '../types'; import { CreateExtensionOptions, - ExtensionDataContainer, ExtensionDefinition, - ResolvedExtensionInput, ResolvedExtensionInputs, VerifyExtensionFactoryOutput, createExtension, @@ -29,10 +27,16 @@ import { z } from 'zod'; import { ExtensionInput } from './createExtensionInput'; import { AnyExtensionDataRef, - ExtensionDataRef, - ExtensionDataRefToValue, ExtensionDataValue, } from './createExtensionDataRef'; +import { + ExtensionDataContainer, + createExtensionDataContainer, +} from './createExtensionDataContainer'; +import { + ResolveInputValueOverrides, + resolveInputOverrides, +} from './resolveInputOverrides'; /** * @public @@ -77,61 +81,6 @@ export type CreateExtensionBlueprintOptions< dataRefs?: TDataRefs; } & VerifyExtensionFactoryOutput; -/** @public */ -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 */ @@ -252,129 +201,6 @@ export interface ExtensionBlueprint< >; } -/** @internal */ -export function createDataContainer( - values: Iterable< - UData extends ExtensionDataRef - ? 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( - `extension data '${output.id}' was provided but 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 extension data value(s) '${remainingRefs - .map(ref => ref.id) - .join(', ')}'`, - ); - } - - return { - get(ref) { - return container.get(ref.id)?.value; - }, - [Symbol.iterator]() { - return container.values(); - }, - } 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( - `attempted to override data of input '${name}' but it is not present in the original inputs`, - ); - } - 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( - `override data provided for input '${name}' must be an array`, - ); - } - 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`, - ); - } - 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 */ @@ -506,7 +332,7 @@ class ExtensionBlueprintImpl< inputs?: ResolveInputValueOverrides; }, ): ExtensionDataContainer => { - return createDataContainer( + return createExtensionDataContainer( this.options.factory(innerParams, { node, config: innerContext?.config ?? config, diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataContainer.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataContainer.ts new file mode 100644 index 0000000000..d79ec70f2f --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataContainer.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AnyExtensionDataRef, + ExtensionDataRef, + ExtensionDataValue, +} from './createExtensionDataRef'; + +/** @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 + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; + }; + +/** @internal */ +export function createExtensionDataContainer( + values: Iterable< + UData extends ExtensionDataRef + ? 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( + `extension data '${output.id}' was provided but 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 extension data value(s) '${remainingRefs + .map(ref => ref.id) + .join(', ')}'`, + ); + } + + return { + get(ref) { + return container.get(ref.id)?.value; + }, + [Symbol.iterator]() { + return container.values(); + }, + } as ExtensionDataContainer; +} diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index a28069da88..e3389593e9 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -17,7 +17,6 @@ export { coreExtensionData } from './coreExtensionData'; export { createExtension, - type ExtensionDataContainer, type ExtensionDefinition, type CreateExtensionOptions, type ExtensionDataValues, @@ -32,6 +31,7 @@ export { type ExtensionInput, type LegacyExtensionInput, } from './createExtensionInput'; +export { type ExtensionDataContainer } from './createExtensionDataContainer'; export { createExtensionDataRef, type AnyExtensionDataRef, @@ -56,7 +56,7 @@ export { } from './types'; export { type CreateExtensionBlueprintOptions, - type ResolveInputValueOverrides, type ExtensionBlueprint, createExtensionBlueprint, } from './createExtensionBlueprint'; +export { type ResolveInputValueOverrides } from './resolveInputOverrides'; diff --git a/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts new file mode 100644 index 0000000000..06143549bb --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/resolveInputOverrides.ts @@ -0,0 +1,162 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppNode } from '../apis'; +import { Expand } from '../types'; +import { ResolvedExtensionInput } from './createExtension'; +import { + ExtensionDataContainer, + createExtensionDataContainer, +} from './createExtensionDataContainer'; +import { + AnyExtensionDataRef, + ExtensionDataRefToValue, + ExtensionDataValue, +} from './createExtensionDataRef'; +import { ExtensionInput } from './createExtensionInput'; + +/** @public */ +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; + } +>; + +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 = createExtensionDataContainer( + providedData as Iterable>, + declaredInput.extensionData, + ); + if (!originalInput) { + throw new Error( + `attempted to override data of input '${name}' but it is not present in the original inputs`, + ); + } + 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( + `override data provided for input '${name}' must be an array`, + ); + } + 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`, + ); + } + newInputs[name] = providedData.map((data, i) => { + const providedContainer = createExtensionDataContainer( + data as Iterable>, + declaredInput.extensionData, + ); + return Object.assign(providedContainer, { + name: (originalInput[i] as ResolvedExtensionInput).node, + }) as any; + }); + } + } + return newInputs; +}