frontend-plugin-api: switch extension factory to receive node instead of spec

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-11-21 20:30:41 +01:00
parent 08db02eda5
commit cc1fea7d05
4 changed files with 62 additions and 59 deletions
@@ -15,6 +15,7 @@
*/
import {
AppNode,
Extension,
createExtension,
createExtensionDataRef,
@@ -52,15 +53,27 @@ const simpleExtension = createExtension({
function makeSpec<TConfig>(
extension: Extension<TConfig>,
config?: TConfig,
spec?: Partial<AppNodeSpec>,
): AppNodeSpec {
return {
id: extension.id,
attachTo: extension.attachTo,
disabled: extension.disabled,
extension,
config,
source: undefined,
...spec,
};
}
function makeNode<TConfig>(
extension: Extension<TConfig>,
spec?: Partial<AppNodeSpec>,
): AppNode {
return {
spec: makeSpec(extension, spec),
edges: {
attachments: new Map(),
},
};
}
@@ -71,7 +84,7 @@ function makeInstanceWithId<TConfig>(
return {
id: extension.id,
instance: createAppNodeInstance({
spec: makeSpec(extension, config),
node: makeNode(extension, { config }),
attachments: new Map(),
}),
};
@@ -80,7 +93,7 @@ function makeInstanceWithId<TConfig>(
describe('instantiateAppNodeTree', () => {
it('should instantiate a single node', () => {
const tree = resolveAppTree('root-node', [
{ ...makeSpec(simpleExtension), id: 'root-node' },
makeSpec(simpleExtension, { id: 'root-node' }),
]);
expect(tree.root.instance).not.toBeDefined();
instantiateAppNodeTree(tree.root);
@@ -94,7 +107,7 @@ describe('instantiateAppNodeTree', () => {
it('should not instantiate disabled nodes', () => {
const tree = resolveAppTree('root-node', [
{ ...makeSpec(simpleExtension), id: 'root-node', disabled: true },
makeSpec(simpleExtension, { id: 'root-node', disabled: true }),
]);
expect(tree.root.instance).not.toBeDefined();
instantiateAppNodeTree(tree.root);
@@ -103,28 +116,25 @@ describe('instantiateAppNodeTree', () => {
it('should instantiate a node with attachments', () => {
const tree = resolveAppTree('root-node', [
{
...makeSpec(
createExtension({
id: 'root-node',
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
test: createExtensionInput({ test: testDataRef }),
},
output: {
inputMirror: inputMirrorDataRef,
},
factory({ inputs }) {
return { inputMirror: inputs };
},
}),
),
},
{
...makeSpec(simpleExtension),
makeSpec(
createExtension({
id: 'root-node',
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
test: createExtensionInput({ test: testDataRef }),
},
output: {
inputMirror: inputMirrorDataRef,
},
factory({ inputs }) {
return { inputMirror: inputs };
},
}),
),
makeSpec(simpleExtension, {
id: 'child-node',
attachTo: { id: 'root-node', input: 'test' },
},
}),
]);
const childNode = tree.nodes.get('child-node');
@@ -191,7 +201,7 @@ describe('createAppNodeInstance', () => {
it('should create a simple extension instance', () => {
const attachments = new Map();
const instance = createAppNodeInstance({
spec: makeSpec(simpleExtension),
node: makeNode(simpleExtension),
attachments,
});
@@ -231,7 +241,7 @@ describe('createAppNodeInstance', () => {
]);
const instance = createAppNodeInstance({
attachments,
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -283,10 +293,7 @@ describe('createAppNodeInstance', () => {
it('should refuse to create an extension with invalid config', () => {
expect(() =>
createAppNodeInstance({
spec: {
...makeSpec(simpleExtension),
config: { other: 'not-a-number' },
},
node: makeNode(simpleExtension, { config: { other: 'not-a-number' } }),
attachments: new Map(),
}),
).toThrow(
@@ -297,7 +304,7 @@ describe('createAppNodeInstance', () => {
it('should forward extension factory errors', () => {
expect(() =>
createAppNodeInstance({
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -319,7 +326,7 @@ describe('createAppNodeInstance', () => {
it('should refuse to create an instance with duplicate output', () => {
expect(() =>
createAppNodeInstance({
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -342,7 +349,7 @@ describe('createAppNodeInstance', () => {
it('should refuse to create an instance with disconnected output data', () => {
expect(() =>
createAppNodeInstance({
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -364,7 +371,7 @@ describe('createAppNodeInstance', () => {
it('should refuse to create an instance with missing required input', () => {
expect(() =>
createAppNodeInstance({
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -408,7 +415,7 @@ describe('createAppNodeInstance', () => {
],
],
]),
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -443,7 +450,7 @@ describe('createAppNodeInstance', () => {
],
],
]),
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -469,7 +476,7 @@ describe('createAppNodeInstance', () => {
],
],
]),
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -503,7 +510,7 @@ describe('createAppNodeInstance', () => {
],
],
]),
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -531,7 +538,7 @@ describe('createAppNodeInstance', () => {
attachments: new Map([
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
]),
spec: makeSpec(
node: makeNode(
createExtension({
id: 'core.test',
attachTo: { id: 'ignored', input: 'ignored' },
@@ -20,11 +20,7 @@ import {
ExtensionDataRef,
} from '@backstage/frontend-plugin-api';
import mapValues from 'lodash/mapValues';
import {
AppNode,
AppNodeInstance,
AppNodeSpec,
} from '@backstage/frontend-plugin-api';
import { AppNode, AppNodeInstance } from '@backstage/frontend-plugin-api';
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
@@ -99,11 +95,11 @@ function resolveInputs(
/** @internal */
export function createAppNodeInstance(options: {
spec: AppNodeSpec;
node: AppNode;
attachments: ReadonlyMap<string, { id: string; instance: AppNodeInstance }[]>;
}): AppNodeInstance {
const { spec, attachments } = options;
const { id, extension, config } = spec;
const { node, attachments } = options;
const { id, extension, config } = node.spec;
const extensionData = new Map<string, unknown>();
const extensionDataRefs = new Set<ExtensionDataRef<unknown>>();
@@ -118,7 +114,7 @@ export function createAppNodeInstance(options: {
try {
const namedOutputs = extension.factory({
spec,
node,
config: parsedConfig,
inputs: resolveInputs(extension.inputs, attachments),
});
@@ -186,7 +182,7 @@ export function instantiateAppNodeTree(rootNode: AppNode): void {
}
(node as Mutable<AppNode>).instance = createAppNodeInstance({
spec: node.spec,
node,
attachments: instantiatedAttachments,
});
@@ -20,7 +20,7 @@ import { ErrorBoundary } from './ErrorBoundary';
import { ExtensionSuspense } from './ExtensionSuspense';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker';
import { AppNodeSpec } from '../apis';
import { AppNode } from '../apis';
type RouteTrackerProps = PropsWithChildren<{
disableTracking?: boolean;
@@ -44,24 +44,24 @@ const RouteTracker = (props: RouteTrackerProps) => {
/** @public */
export interface ExtensionBoundaryProps {
spec: AppNodeSpec;
node: AppNode;
routable?: boolean;
children: ReactNode;
}
/** @public */
export function ExtensionBoundary(props: ExtensionBoundaryProps) {
const { spec, routable, children } = props;
const { node, routable, children } = props;
// Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight
const attributes = {
extension: spec.id,
pluginId: spec.source?.id,
extension: node.spec.id,
pluginId: node.spec.source?.id,
};
return (
<ExtensionSuspense>
<ErrorBoundary plugin={spec.source}>
<ErrorBoundary plugin={node.spec.source}>
<AnalyticsContext attributes={attributes}>
<RouteTracker disableTracking={!routable}>{children}</RouteTracker>
</AnalyticsContext>
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { AppNodeSpec } from '../apis';
import { AppNode } from '../apis';
import { PortableSchema } from '../schema';
import { Expand } from '../types';
import { ExtensionDataRef } from './createExtensionDataRef';
@@ -80,7 +80,7 @@ export interface CreateExtensionOptions<
output: TOutput;
configSchema?: PortableSchema<TConfig>;
factory(options: {
spec: AppNodeSpec;
node: AppNode;
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}): Expand<ExtensionDataValues<TOutput>>;
@@ -96,7 +96,7 @@ export interface Extension<TConfig> {
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
spec: AppNodeSpec;
node: AppNode;
config: TConfig;
inputs: Record<
string,