diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 6efe3984dc..f48aa2ea6b 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -22,7 +22,7 @@ import { export const Core = createExtension({ id: 'core', - attachTo: { id: 'root', input: 'default' }, + attachTo: { id: 'root', input: 'default' }, // ignored inputs: { apis: createExtensionInput({ api: coreExtensionData.apiFactory, diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts index 21a5009a95..dc489565f4 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.test.ts @@ -19,66 +19,70 @@ import { buildAppGraph } from './buildAppGraph'; const extBaseConfig = { id: 'test', - attachTo: { id: 'root', input: 'default' }, + attachTo: { id: 'nonexistent', input: 'nonexistent' }, output: {}, factory() {}, }; const extension = createExtension(extBaseConfig); -const baseSpec = { extension, disabled: false }; +const baseSpec = { + extension, + attachTo: { id: 'nonexistent', input: 'nonexistent' }, + disabled: false, +}; describe('buildAppGraph', () => { - it('creates an empty graph', () => { - const graph = buildAppGraph([]); - expect([...graph.rootNodes.keys()]).toEqual([]); - expect([...graph.orphanNodes.keys()]).toEqual([]); + it('should fail to create an empty graph', () => { + expect(() => buildAppGraph([])).toThrow( + "No root node with id 'core' found in app graph", + ); + }); + + it('should create a graph with only one node', () => { + const graph = buildAppGraph([{ ...baseSpec, id: 'core' }]); + expect(graph.root).toEqual({ + spec: { ...baseSpec, id: 'core' }, + edges: { attachments: new Map() }, + }); + expect(Array.from(graph.orphans)).toEqual([]); }); it('should create a graph', () => { - const graph = buildAppGraph([ - { ...baseSpec, id: 'a' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); - expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); - expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); - - expect(JSON.parse(JSON.stringify([...graph.rootNodes.values()]))) - .toMatchInlineSnapshot(` + const graph = buildAppGraph( [ - { - "id": "a", + { ...baseSpec, id: 'a' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + 'b', + ); + + expect(JSON.parse(JSON.stringify(graph.root))).toMatchInlineSnapshot(` + { + "attachments": { + "x": [ + { + "id": "bx1", + }, + { + "id": "bx2", + }, + ], + "y": [ + { + "id": "by1", + }, + ], }, - { - "attachments": { - "x": [ - { - "id": "bx1", - }, - { - "id": "bx2", - }, - ], - "y": [ - { - "id": "by1", - }, - ], - }, - "id": "b", - }, - { - "id": "c", - }, - ] + "id": "b", + } `); - expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`""`); - expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(` + expect(String(graph.root)).toMatchInlineSnapshot(` " x [ @@ -89,24 +93,32 @@ describe('buildAppGraph', () => { ] " `); - expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`""`); + + const orphans = Array.from(graph.orphans).map(String); + expect(orphans).toMatchInlineSnapshot(` + [ + "", + "", + "", + ] + `); }); it('should create a graph out of order', () => { - const graph = buildAppGraph([ - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, - { ...baseSpec, id: 'a' }, - { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, - { ...baseSpec, id: 'b' }, - { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, - { ...baseSpec, id: 'c' }, - { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, - ]); - expect([...graph.rootNodes.keys()]).toEqual(['a', 'b', 'c']); - expect([...graph.orphanNodes.keys()]).toEqual(['dx1']); + const graph = buildAppGraph( + [ + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx2' }, + { ...baseSpec, id: 'a' }, + { ...baseSpec, attachTo: { id: 'b', input: 'y' }, id: 'by1' }, + { ...baseSpec, id: 'b' }, + { ...baseSpec, attachTo: { id: 'b', input: 'x' }, id: 'bx1' }, + { ...baseSpec, id: 'c' }, + { ...baseSpec, attachTo: { id: 'd', input: 'x' }, id: 'dx1' }, + ], + 'b', + ); - expect(String(graph.rootNodes.get('a'))).toMatchInlineSnapshot(`""`); - expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(` + expect(String(graph.root)).toMatchInlineSnapshot(` " x [ @@ -117,7 +129,15 @@ describe('buildAppGraph', () => { ] " `); - expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`""`); + + const orphans = Array.from(graph.orphans).map(String); + expect(orphans).toMatchInlineSnapshot(` + [ + "", + "", + "", + ] + `); }); it('throws an error when duplicated extensions are detected', () => { diff --git a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts index 79977f6993..72d4d90d48 100644 --- a/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts +++ b/packages/frontend-app-api/src/wiring/graph/buildAppGraph.ts @@ -78,9 +78,15 @@ class SerializableAppNode implements AppNode { * tree with all attachments in the same order as they appear in the input specs array. * @internal */ -export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { +export function buildAppGraph( + specs: AppNodeSpec[], + rootNodeId = 'core', +): AppGraph { const nodes = new Map(); - const rootNodes = new Map(); + + // A node with the provided rootNodeId must be found in the graph, and it must not be attached to anything + let rootNode: AppNode | undefined = undefined; + // While iterating through the inputs specs we keep track of all nodes that were created // before their parent, and attach them later when the parent is created. // As we find the parents and attach the children, we remove them from this map. This means @@ -99,7 +105,10 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { const node = new SerializableAppNode(spec); nodes.set(spec.id, node); - if (spec.attachTo) { + // TODO: For now we simply ignore the attachTo spec of the root node, but it'd be cleaner if we could avoid defining it + if (spec.id === rootNodeId) { + rootNode = node; + } else { const parent = nodes.get(spec.attachTo.id); if (parent) { (node.edges as Mutable).attachedTo = { @@ -123,8 +132,6 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { orphansByParent.set(spec.attachTo.id, [orphan]); } } - } else { - rootNodes.set(spec.id, node); } const orphanedChildren = orphansByParent.get(spec.id); @@ -142,11 +149,13 @@ export function buildAppGraph(specs: AppNodeSpec[]): AppGraph { } } - const orphanNodes = new Map( - Array.from(orphansByParent).flatMap(([, orphans]) => - orphans.map(({ orphan }) => [orphan.spec.id, orphan]), - ), + const orphanNodes = Array.from(orphansByParent).flatMap(([, orphans]) => + orphans.map(({ orphan }) => orphan), ); - return { rootNodes, orphanNodes }; + if (!rootNode) { + throw new Error(`No root node with id '${rootNodeId}' found in app graph`); + } + + return { root: rootNode, orphans: orphanNodes }; } diff --git a/packages/frontend-app-api/src/wiring/graph/types.ts b/packages/frontend-app-api/src/wiring/graph/types.ts index 29c0028d66..6cf4ade360 100644 --- a/packages/frontend-app-api/src/wiring/graph/types.ts +++ b/packages/frontend-app-api/src/wiring/graph/types.ts @@ -31,7 +31,7 @@ export type Mutable = { */ export interface AppNodeSpec { readonly id: string; - readonly attachTo?: { id: string; input: string }; + readonly attachTo: { id: string; input: string }; readonly extension: Extension; readonly disabled: boolean; readonly config?: unknown; @@ -70,6 +70,6 @@ export interface AppNode { } export interface AppGraph { - rootNodes: Map; - orphanNodes: Map; + root: AppNode; + orphans: Iterable; }