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;
}