diff --git a/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts b/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts deleted file mode 100644 index 22cb3d4df7..0000000000 --- a/packages/frontend-app-api/src/wiring/graph/createAppNodeInstance.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2023 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 { - AnyExtensionDataMap, - AnyExtensionInputMap, - ExtensionDataRef, -} from '@backstage/frontend-plugin-api'; -import mapValues from 'lodash/mapValues'; -import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; - -type AppNodeWithInstance = AppNode & { instance: AppNodeInstance }; - -function resolveInputData( - dataMap: AnyExtensionDataMap, - attachment: AppNodeWithInstance, - inputName: string, -) { - return mapValues(dataMap, 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.spec.id}'`, - ); - } - return value; - }); -} - -function resolveInputs( - inputMap: AnyExtensionInputMap, - attachments: ReadonlyMap, -) { - const undeclaredAttachments = Array.from(attachments.entries()).filter( - ([inputName]) => inputMap[inputName] === undefined, - ); - // TODO: Make this a warning rather than an error - if (undeclaredAttachments.length > 0) { - throw new Error( - `received undeclared input${ - undeclaredAttachments.length > 1 ? 's' : '' - } ${undeclaredAttachments - .map( - ([k, exts]) => - `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts - .map(e => e.spec.id) - .join("', '")}'`, - ) - .join(' and ')}`, - ); - } - - return mapValues(inputMap, (input, inputName) => { - const attachedNodes = - attachments - .get(inputName) - ?.filter((node): node is AppNodeWithInstance => - Boolean(node.instance), - ) ?? []; - - if (input.config.singleton) { - 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: '${attachedNodeIds.join( - "', '", - )}'`, - ); - } 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, attachedNodes[0], inputName); - } - - return attachedNodes.map(attachment => - resolveInputData(input.extensionData, attachment, inputName), - ); - }); -} - -/** @internal */ -export function createAppNodeInstance(options: { - spec: AppNodeSpec; - attachments: ReadonlyMap; -}): 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 '${id}'; caused by ${e}`, - ); - } - - try { - extension.factory({ - source, - config: parsedConfig, - bind: namedOutputs => { - for (const [name, output] of Object.entries(namedOutputs)) { - const ref = extension.output[name]; - if (!ref) { - throw new Error(`unknown output provided via '${name}'`); - } - if (extensionData.has(ref.id)) { - throw new Error( - `duplicate extension data '${ref.id}' received via output '${name}'`, - ); - } - extensionData.set(ref.id, output); - extensionDataRefs.add(ref); - } - }, - inputs: resolveInputs(extension.inputs, attachments), - }); - } catch (e) { - throw new Error( - `Failed to instantiate extension '${id}'${ - e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` - }`, - ); - } - - return { - getDataRefs() { - return extensionDataRefs.values(); - }, - getData(ref: ExtensionDataRef): T | undefined { - return extensionData.get(ref.id) as T | undefined; - }, - }; -} diff --git a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts index b01ad020b9..ba0c85692e 100644 --- a/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/wiring/graph/instantiateAppNodeTree.ts @@ -14,30 +14,178 @@ * limitations under the License. */ -import { createAppNodeInstance } from './createAppNodeInstance'; -import { AppNode, Mutable } from './types'; +import { + AnyExtensionDataMap, + AnyExtensionInputMap, + ExtensionDataRef, +} from '@backstage/frontend-plugin-api'; +import mapValues from 'lodash/mapValues'; +import { AppNode, AppNodeInstance, AppNodeSpec } from './types'; +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +function resolveInputData( + dataMap: AnyExtensionDataMap, + attachment: { id: string; instance: AppNodeInstance }, + inputName: string, +) { + return mapValues(dataMap, 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}'`, + ); + } + return value; + }); +} + +function resolveInputs( + inputMap: AnyExtensionInputMap, + attachments: ReadonlyMap, +) { + const undeclaredAttachments = Array.from(attachments.entries()).filter( + ([inputName]) => inputMap[inputName] === undefined, + ); + // TODO: Make this a warning rather than an error + if (undeclaredAttachments.length > 0) { + throw new Error( + `received undeclared input${ + undeclaredAttachments.length > 1 ? 's' : '' + } ${undeclaredAttachments + .map( + ([k, exts]) => + `'${k}' from extension${exts.length > 1 ? 's' : ''} '${exts + .map(e => e.id) + .join("', '")}'`, + ) + .join(' and ')}`, + ); + } + + return mapValues(inputMap, (input, inputName) => { + const attachedNodes = attachments.get(inputName) ?? []; + + if (input.config.singleton) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.id); + throw Error( + `expected ${ + input.config.optional ? 'at most' : 'exactly' + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, + ); + } 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, attachedNodes[0], inputName); + } + + return attachedNodes.map(attachment => + resolveInputData(input.extensionData, attachment, inputName), + ); + }); +} + +/** @internal */ +export function createAppNodeInstance(options: { + spec: AppNodeSpec; + attachments: ReadonlyMap; +}): 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 '${id}'; caused by ${e}`, + ); + } + + try { + extension.factory({ + source, + config: parsedConfig, + bind: namedOutputs => { + for (const [name, output] of Object.entries(namedOutputs)) { + const ref = extension.output[name]; + if (!ref) { + throw new Error(`unknown output provided via '${name}'`); + } + if (extensionData.has(ref.id)) { + throw new Error( + `duplicate extension data '${ref.id}' received via output '${name}'`, + ); + } + extensionData.set(ref.id, output); + extensionDataRefs.add(ref); + } + }, + inputs: resolveInputs(extension.inputs, attachments), + }); + } catch (e) { + throw new Error( + `Failed to instantiate extension '${id}'${ + e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}` + }`, + ); + } + + return { + getDataRefs() { + return extensionDataRefs.values(); + }, + getData(ref: ExtensionDataRef): T | undefined { + return extensionData.get(ref.id) as T | undefined; + }, + }; +} + +/** + * Starting at the provided node, instantiate all reachable nodes in the graph that have not been disabled. + * @internal + */ export function instantiateAppNodeTree(rootNode: AppNode): void { - function createInstance(node: AppNode): void { + function createInstance(node: AppNode): AppNodeInstance | undefined { if (node.instance) { - return; + return node.instance; } if (node.spec.disabled) { - return; + return undefined; } - for (const children of node.edges.attachments.values()) { - for (const child of children) { - createInstance(child); - } + const instantiatedAttachments = new Map< + string, + { id: string; instance: AppNodeInstance }[] + >(); + + for (const [input, children] of node.edges.attachments) { + const instantiatedChildren = children.flatMap(child => { + const childInstance = createInstance(child); + if (!childInstance) { + return []; + } + return [{ id: child.spec.id, instance: childInstance }]; + }); + instantiatedAttachments.set(input, instantiatedChildren); } (node as Mutable).instance = createAppNodeInstance({ spec: node.spec, - attachments: node.edges.attachments, + attachments: instantiatedAttachments, }); - return; + return node.instance; } createInstance(rootNode); diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index d7f80faa1b..b828c9762e 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -20,11 +20,6 @@ import { ExtensionDataRef, } from '@backstage/frontend-plugin-api'; -/** @internal */ -export type Mutable = { - -readonly [P in keyof T]: T[P]; -}; - /** * The specification for this node in the app graph. * @public