frontend-app-api: refactor createExtensionInstance -> createAppNodeInstance

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-10-18 01:04:50 +02:00
parent 544968da08
commit bb4d40b481
@@ -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<string, ExtensionInstance[]>,
attachments: Map<string, AppNode[]>,
) {
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<string, unknown>;
readonly attachments: Map<string, ExtensionInstance[]>;
readonly source?: BackstagePlugin;
constructor(
id: string,
extensionData: Map<string, unknown>,
attachments: Map<string, ExtensionInstance[]>,
source: BackstagePlugin | undefined,
) {
this.id = id;
this.#extensionData = extensionData;
this.attachments = attachments;
this.source = source;
}
getData<T>(ref: ExtensionDataRef<T>): 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')),
),
`</${this.id}>`,
].join('\n');
}
}
/** @internal */
export function createExtensionInstance(options: {
extension: Extension<unknown>;
config: unknown;
source?: BackstagePlugin;
attachments: Map<string, ExtensionInstance[]>;
}): ExtensionInstance {
const { extension, config, source, attachments } = options;
export function createAppNodeInstance(options: {
spec: AppNodeSpec;
attachments: Map<string, AppNode[]>;
}): AppNodeInstance {
const { spec, attachments } = options;
const { id, extension, config, source } = spec;
const extensionData = new Map<string, unknown>();
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
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<T>(ref: ExtensionDataRef<T>): T | undefined {
return extensionData.get(ref.id) as T | undefined;
},
};
}