frontend-app-api: tweak app graph to have an explicit root node
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(`"<a />"`);
|
||||
expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(`
|
||||
expect(String(graph.root)).toMatchInlineSnapshot(`
|
||||
"<b>
|
||||
x [
|
||||
<bx1 />
|
||||
@@ -89,24 +93,32 @@ describe('buildAppGraph', () => {
|
||||
]
|
||||
</b>"
|
||||
`);
|
||||
expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`"<c />"`);
|
||||
|
||||
const orphans = Array.from(graph.orphans).map(String);
|
||||
expect(orphans).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<a />",
|
||||
"<c />",
|
||||
"<dx1 />",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
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(`"<a />"`);
|
||||
expect(String(graph.rootNodes.get('b'))).toMatchInlineSnapshot(`
|
||||
expect(String(graph.root)).toMatchInlineSnapshot(`
|
||||
"<b>
|
||||
x [
|
||||
<bx2 />
|
||||
@@ -117,7 +129,15 @@ describe('buildAppGraph', () => {
|
||||
]
|
||||
</b>"
|
||||
`);
|
||||
expect(String(graph.rootNodes.get('c'))).toMatchInlineSnapshot(`"<c />"`);
|
||||
|
||||
const orphans = Array.from(graph.orphans).map(String);
|
||||
expect(orphans).toMatchInlineSnapshot(`
|
||||
[
|
||||
"<a />",
|
||||
"<c />",
|
||||
"<dx1 />",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws an error when duplicated extensions are detected', () => {
|
||||
|
||||
@@ -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<string, AppNode>();
|
||||
const rootNodes = new Map<string, AppNode>();
|
||||
|
||||
// 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<AppNodeEdges>).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 };
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export type Mutable<T> = {
|
||||
*/
|
||||
export interface AppNodeSpec {
|
||||
readonly id: string;
|
||||
readonly attachTo?: { id: string; input: string };
|
||||
readonly attachTo: { id: string; input: string };
|
||||
readonly extension: Extension<unknown>;
|
||||
readonly disabled: boolean;
|
||||
readonly config?: unknown;
|
||||
@@ -70,6 +70,6 @@ export interface AppNode {
|
||||
}
|
||||
|
||||
export interface AppGraph {
|
||||
rootNodes: Map<string, AppNode>;
|
||||
orphanNodes: Map<string, AppNode>;
|
||||
root: AppNode;
|
||||
orphans: Iterable<AppNode>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user