From bb4d40b481d92f9e85157a20bc18064a7fb69adc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Oct 2023 01:04:50 +0200 Subject: [PATCH] frontend-app-api: refactor createExtensionInstance -> createAppNodeInstance Signed-off-by: Patrik Oldsberg --- ...onInstance.ts => createAppNodeInstance.ts} | 136 ++++++------------ 1 file changed, 41 insertions(+), 95 deletions(-) rename packages/frontend-app-api/src/wiring/graph/{createExtensionInstance.ts => createAppNodeInstance.ts} (58%) diff --git a/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts similarity index 58% rename from packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts rename to packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts index 767329da7f..310852cd1c 100644 --- a/packages/frontend-app-api/src/wiring/graph/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts @@ -18,10 +18,12 @@ import { AnyExtensionDataMap, AnyExtensionInputMap, BackstagePlugin, - Extension, ExtensionDataRef, } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; +import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; + +type AppNodeWithInstance = AppNode & { instance: AppNodeInstance }; /** @internal */ export interface ExtensionInstance { @@ -42,14 +44,14 @@ export interface ExtensionInstance { function resolveInputData( dataMap: AnyExtensionDataMap, - attachment: ExtensionInstance, + attachment: AppNodeWithInstance, inputName: string, ) { return mapValues(dataMap, ref => { - const value = attachment.getData(ref); + const value = attachment.instance.getData(ref); if (value === undefined && !ref.config.optional) { throw new Error( - `input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.id}'`, + `input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.spec.id}'`, ); } return value; @@ -58,7 +60,7 @@ function resolveInputData( function resolveInputs( inputMap: AnyExtensionInputMap, - attachments: Map, + attachments: Map, ) { const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, @@ -72,7 +74,7 @@ function resolveInputs( .map( ([k, exts]) => `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts - .map(e => e.id) + .map(e => e.spec.id) .join("', '")}'`, ) .join(' and ')}`, @@ -80,113 +82,54 @@ function resolveInputs( } return mapValues(inputMap, (input, inputName) => { - const attachedInstances = attachments.get(inputName) ?? []; + const attachedNodes = + attachments + .get(inputName) + ?.filter((node): node is AppNodeWithInstance => + Boolean(node.instance), + ) ?? []; + if (input.config.singleton) { - if (attachedInstances.length > 1) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.spec.id); throw Error( `expected ${ input.config.optional ? 'at most' : 'exactly' - } one '${inputName}' input but received multiple: '${attachedInstances - .map(e => e.id) - .join("', '")}'`, + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, ); - } else if (attachedInstances.length === 0) { + } else if (attachedNodes.length === 0) { if (input.config.optional) { return undefined; } throw Error(`input '${inputName}' is required but was not received`); } - return resolveInputData( - input.extensionData, - attachedInstances[0], - inputName, - ); + return resolveInputData(input.extensionData, attachedNodes[0], inputName); } - return attachedInstances.map(attachment => + return attachedNodes.map(attachment => resolveInputData(input.extensionData, attachment, inputName), ); }); } -function indent(str: string) { - return str.replace(/^/gm, ' '); -} - -class ExtensionInstanceImpl implements ExtensionInstance { - readonly $$type = '@backstage/ExtensionInstance'; - - readonly id: string; - readonly #extensionData: Map; - readonly attachments: Map; - readonly source?: BackstagePlugin; - - constructor( - id: string, - extensionData: Map, - attachments: Map, - source: BackstagePlugin | undefined, - ) { - this.id = id; - this.#extensionData = extensionData; - this.attachments = attachments; - this.source = source; - } - - getData(ref: ExtensionDataRef): T | undefined { - return this.#extensionData.get(ref.id) as T | undefined; - } - - toJSON() { - return { - id: this.id, - output: - this.#extensionData.size > 0 - ? [...this.#extensionData.keys()] - : undefined, - attachments: - this.attachments.size > 0 - ? Object.fromEntries(this.attachments) - : undefined, - }; - } - - toString() { - const out = - this.#extensionData.size > 0 - ? ` out=[${[...this.#extensionData.keys()].join(', ')}]` - : ''; - - if (this.attachments.size === 0) { - return `<${this.id}${out} />`; - } - - return [ - `<${this.id}${out}>`, - ...[...this.attachments.entries()].map(([k, v]) => - indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')), - ), - ``, - ].join('\n'); - } -} - /** @internal */ -export function createExtensionInstance(options: { - extension: Extension; - config: unknown; - source?: BackstagePlugin; - attachments: Map; -}): ExtensionInstance { - const { extension, config, source, attachments } = options; +export function createAppNodeInstance(options: { + spec: AppNodeSpec; + attachments: Map; +}): AppNodeInstance { + const { spec, attachments } = options; + const { id, extension, config, source } = spec; const extensionData = new Map(); + const extensionDataRefs = new Set>(); let parsedConfig: unknown; try { parsedConfig = extension.configSchema?.parse(config ?? {}); } catch (e) { throw new Error( - `Invalid configuration for extension '${extension.id}'; caused by ${e}`, + `Invalid configuration for extension '${id}'; caused by ${e}`, ); } @@ -206,22 +149,25 @@ export function createExtensionInstance(options: { ); } extensionData.set(ref.id, output); + extensionDataRefs.add(ref); } }, inputs: resolveInputs(extension.inputs, attachments), }); } catch (e) { throw new Error( - `Failed to instantiate extension '${extension.id}'${ + `Failed to instantiate extension '${id}'${ e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` }`, ); } - return new ExtensionInstanceImpl( - options.extension.id, - extensionData, - attachments, - source, - ); + return { + getDataRefs() { + return extensionDataRefs.values(); + }, + getData(ref: ExtensionDataRef): T | undefined { + return extensionData.get(ref.id) as T | undefined; + }, + }; }