frontend-app-api: refactor createExtensionInstance -> createAppNodeInstance
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
+41
-95
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user