refactor(devtools): move identity tracking to separate abstraction

This commit is contained in:
mgechev 2020-02-18 19:04:34 -08:00 committed by Sumit Arora
parent 4d8ca6565a
commit 03f896c48e
8 changed files with 409 additions and 165 deletions

View file

@ -7,7 +7,7 @@ export const onChangeDetection = (callback: () => void): void => {
if (hookInitialized) {
return;
}
const forest = getDirectiveForest();
const forest = getDirectiveForest(document.documentElement, (window as any).ng);
listenAndNotifyOnUpdates(forest, callback);
hookInitialized = true;
};

View file

@ -59,7 +59,7 @@ const initChangeDetection = (messageBus: MessageBus<Events>) => {
const getLatestComponentExplorerViewCallback = (messageBus: MessageBus<Events>) => query => {
messageBus.emit('latestComponentExplorerView', [
{
forest: prepareForestForSerialization(getDirectiveForest()),
forest: prepareForestForSerialization(getDirectiveForest(document.documentElement, (window as any).ng)),
properties: getLatestComponentState(query),
},
]);
@ -74,7 +74,7 @@ const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
};
const getElementDirectivesPropertiesCallback = (messageBus: MessageBus<Events>) => (id: ElementID) => {
const node = queryComponentForest(id, getDirectiveForest());
const node = queryComponentForest(id, getDirectiveForest(document.documentElement, (window as any).ng));
if (node) {
messageBus.emit('elementDirectivesProperties', [serializeNodeDirectiveProperties(node)]);
} else {
@ -83,12 +83,12 @@ const getElementDirectivesPropertiesCallback = (messageBus: MessageBus<Events>)
};
const selectedComponentCallback = (id: ElementID) => {
const node = queryComponentForest(id, getDirectiveForest());
const node = queryComponentForest(id, getDirectiveForest(document.documentElement, (window as any).ng));
setConsoleReference(node);
};
const getNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (id: DirectiveID, propPath: string[]) => {
const node = queryComponentForest(id.element, getDirectiveForest());
const node = queryComponentForest(id.element, getDirectiveForest(document.documentElement, (window as any).ng));
if (node) {
let current = (id.directive === undefined ? node.component : node.directives[id.directive]).instance;
for (const prop of propPath) {

View file

@ -61,7 +61,7 @@ export class ComponentInspector {
unHighlight();
if (this._selectedComponent.component) {
highlight(this._selectedComponent.host);
const forest: IndexedNode[] = indexForest(getDirectiveForest());
const forest: IndexedNode[] = indexForest(getDirectiveForest(document.documentElement, (window as any).ng));
const elementId: ElementID = getIndexForNativeElementInForest(this._selectedComponent.host, forest);
this._onComponentEnter(elementId);
}
@ -81,7 +81,7 @@ export class ComponentInspector {
}
highlightById(id: ElementID): void {
const forest: ComponentTreeNode[] = getDirectiveForest();
const forest: ComponentTreeNode[] = getDirectiveForest(document.documentElement, (window as any).ng);
const elementToHighlight: HTMLElement = findNodeInForest(id, forest);
highlight(elementToHighlight);
}

View file

@ -1,7 +1,5 @@
import { deeplySerializeSelectedProperties } from './state-serializer/state-serializer';
declare const ng: any;
import {
ComponentType,
DirectiveType,
@ -12,6 +10,7 @@ import {
} from 'protocol';
import { getComponentName } from './highlighter';
import { IndexedNode } from './recording/observer';
import { DebuggingAPI } from './interfaces';
export interface DirectiveInstanceType extends DirectiveType {
instance: any;
@ -32,7 +31,10 @@ export interface DirectiveForestBuilderOptions {
export const getLatestComponentState = (query: ComponentExplorerViewQuery): DirectivesProperties | undefined => {
let result: DirectivesProperties | undefined;
if (query.selectedElement && query.expandedProperties) {
const node = queryComponentForest(query.selectedElement, getDirectiveForest());
const node = queryComponentForest(
query.selectedElement,
getDirectiveForest(document.documentElement, (window as any).ng)
);
if (!node) {
return undefined;
}
@ -80,20 +82,22 @@ export const prepareForestForSerialization = (roots: ComponentTreeNode[]): Compo
});
};
export const getDirectiveForest = (root = document.documentElement): ComponentTreeNode[] =>
export const getDirectiveForest = (root: HTMLElement, ngd: DebuggingAPI): ComponentTreeNode[] =>
buildDirectiveForest(
root,
{ element: '__ROOT__', component: null, directives: [], children: [] },
{ getDirectives: true }
{ getDirectives: true },
ngd
);
export const getComponentForest = (root = document.documentElement): ComponentTreeNode[] =>
buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] });
export const getComponentForest = (root: HTMLElement, ngd: DebuggingAPI): ComponentTreeNode[] =>
buildDirectiveForest(root, { element: '__ROOT__', component: null, directives: [], children: [] }, {}, ngd);
const buildDirectiveForest = (
node: Element,
tree: ComponentTreeNode | undefined,
options: DirectiveForestBuilderOptions = {}
options: DirectiveForestBuilderOptions = {},
ngd: DebuggingAPI
): ComponentTreeNode[] => {
if (!node) {
return [tree];
@ -101,17 +105,17 @@ const buildDirectiveForest = (
let dirs = [];
if (tree.element !== '__ROOT__' && options.getDirectives) {
// Need to make sure we're in a component tree
// otherwise, ng.getDirectives will throw without
// otherwise, ngd.getDirectives will throw without
// a root node.
try {
dirs = ng.getDirectives(node) || [];
dirs = ngd.getDirectives(node) || [];
} catch (e) {
console.warn('Cannot find context for element', node);
}
}
const cmp = ng.getComponent(node);
const cmp = ngd.getComponent(node);
if (!cmp && !dirs.length) {
Array.from(node.children).forEach(c => buildDirectiveForest(c, tree, options));
Array.from(node.children).forEach(c => buildDirectiveForest(c, tree, options, ngd));
return tree.children;
}
const current: ComponentTreeNode = {
@ -137,7 +141,7 @@ const buildDirectiveForest = (
current.element = node.tagName.toLowerCase();
}
tree.children.push(current);
Array.from(node.children).forEach(c => buildDirectiveForest(c, current, options));
Array.from(node.children).forEach(c => buildDirectiveForest(c, current, options, ngd));
return tree.children;
};
@ -175,14 +179,15 @@ const findElementIDFromNativeElementInForest = (
forest: IndexedNode[],
nativeElement: HTMLElement
): ElementID | null => {
for (let i = 0; i < forest.length; i++) {
if (forest[i].nativeElement === nativeElement) {
return forest[i].id;
for (const el of forest) {
if (el.nativeElement === nativeElement) {
return el.id;
}
}
for (let i = 0; i < forest.length; i++) {
if (forest[i].children.length) {
return findElementIDFromNativeElementInForest(forest[i].children, nativeElement);
for (const el of forest) {
if (el.children.length) {
return findElementIDFromNativeElementInForest(el.children, nativeElement);
}
}
return null;
@ -190,5 +195,5 @@ const findElementIDFromNativeElementInForest = (
export const findNodeFromSerializedPathId = (serializedId: string) => {
const id: number[] = serializedId.split(',').map(index => parseInt(index, 10));
return queryComponentForest(id, getDirectiveForest());
return queryComponentForest(id, getDirectiveForest(document.documentElement, (window as any).ng));
};

View file

@ -0,0 +1,5 @@
export interface DebuggingAPI {
getComponent(node: Node): any;
getDirectives(node: Node): any[];
getHostElement(cmp: any): HTMLElement;
}

View file

@ -0,0 +1,139 @@
import { IdentityTracker } from './identity-tracker';
import { DebuggingAPI } from '../interfaces';
import { DirectiveInstanceType } from '../component-tree';
import { debug } from 'ng-packagr/lib/utils/log';
let debuggingAPI: DebuggingAPI = {
getComponent(node: Node): any {},
getDirectives(node: Node): any[] {
return [];
},
getHostElement(cmp: any): HTMLElement {
return null;
},
};
describe('identity tracker', () => {
let tracker: IdentityTracker;
beforeEach(() => {
tracker = new IdentityTracker(debuggingAPI);
});
it('should index trees', () => {
const dirInstance = {};
const dir: DirectiveInstanceType = {
instance: dirInstance,
name: 'DIR',
};
const cmpInstance = {};
const nested = {
id: [0, 0],
element: 'CMP2',
component: {
name: 'CMP2',
instance: cmpInstance,
},
directives: [dir],
nativeElement: undefined,
children: [],
};
tracker.index({
children: [nested],
nativeElement: undefined,
directives: [],
component: {
instance: {},
name: 'CMP1',
},
element: 'CMP',
id: [0],
});
expect(tracker.getDirectiveID(dirInstance)).toEqual([0, 0]);
expect(tracker.getDirectiveID(cmpInstance)).toEqual([0, 0]);
});
it('should update indexes on insertion', () => {
const childEl = {
children: [],
parentElement: null,
tagName: 'child',
};
const childCmp = {
name: 'childCmp',
};
const siblingEl = {
children: [],
parentElement: null,
tagName: 'sibling',
};
const siblingCmp = {
name: 'siblingCmp',
};
const rootEl = {
children: [childEl, siblingEl],
tagName: 'parent',
};
const rootCmp = {
name: 'rootCmp',
};
childEl.parentElement = rootEl;
siblingEl.parentElement = rootEl;
const nested = {
id: [0, 0],
element: 'CMP2',
component: {
name: 'CMP2',
instance: childCmp,
},
directives: [],
nativeElement: childEl as any,
children: [],
};
const nodeComponent = new Map<any, any>();
nodeComponent.set(rootEl, rootCmp);
nodeComponent.set(childEl, childCmp);
nodeComponent.set(siblingEl, siblingCmp);
const componentNode = new Map<any, any>();
componentNode.set(rootCmp, rootEl);
componentNode.set(childCmp, childEl);
componentNode.set(siblingCmp, siblingEl);
debuggingAPI = {
getComponent(node: Node): any {
return nodeComponent.get(node);
},
getDirectives(node: Node): any[] {
return [];
},
getHostElement(cmp: any): HTMLElement {
return componentNode.get(cmp);
},
};
tracker = new IdentityTracker(debuggingAPI);
tracker.index({
children: [nested],
nativeElement: rootEl as any,
directives: [],
component: {
instance: rootCmp,
name: 'CMP1',
},
element: 'CMP',
id: [0],
});
expect(tracker.getDirectiveID(rootCmp)).toEqual([0]);
tracker.insert(siblingEl as any, siblingCmp);
expect(tracker.getDirectiveID(siblingCmp)).toEqual([0, 1]);
});
});

View file

@ -0,0 +1,176 @@
import { ElementID } from 'protocol';
import { getComponentForest } from '../component-tree';
import { Type } from '@angular/core';
import { IndexedNode } from './observer';
import { DebuggingAPI } from '../interfaces';
interface TreeNode {
parent: TreeNode;
component?: Type<any>;
directives?: Type<any>[];
children: TreeNode[];
}
export class IdentityTracker {
private _elementComponent = new Map<Node, any>();
private _elementDirectives = new Map<Node, any[]>();
private _currentDirectiveID = new Map<any, ElementID>();
private _createdDirectives = new Set<any>();
private _forest: TreeNode[] = [];
private _componentTreeNode = new Map<any, TreeNode>();
constructor(private _ng: DebuggingAPI) {}
getDirectiveID(dir: any) {
return this._currentDirectiveID.get(dir);
}
insert(node: HTMLElement, cmpOrDirective: any | any[]): void {
const isComponent = !Array.isArray(cmpOrDirective);
const parent = getParentComponentFromDomNode(node, this._ng);
(isComponent ? [cmpOrDirective] : cmpOrDirective).forEach((dir: any) => {
this._elementComponent.set(node, dir);
this._createdDirectives.add(dir);
});
let parentTreeNode = null;
let parentID = [];
let childIdx = 0;
let siblingsArray = this._forest;
if (parent) {
const parentElement = this._ng.getHostElement(parent) || document.documentElement;
const children = getComponentForest(parentElement, this._ng)[0].children;
parentID = this._currentDirectiveID.get(parent);
parentTreeNode = this._componentTreeNode.get(parent);
siblingsArray = parentTreeNode.children;
if (isComponent) {
for (const child of children) {
if (child.component.instance === cmpOrDirective) {
break;
}
if (this._createdDirectives.has(child.component.instance)) {
childIdx++;
}
}
} else {
for (const child of children) {
if (child.directives && child.directives.length && child.directives.some(d => d === cmpOrDirective[0])) {
break;
}
if (this._createdDirectives.has(child.component.instance)) {
childIdx++;
}
}
}
}
const treeNode: TreeNode = {
parent: parentTreeNode,
children: [],
directives: isComponent ? undefined : cmpOrDirective,
component: isComponent ? cmpOrDirective : cmpOrDirective[0],
};
siblingsArray.splice(childIdx, 0, treeNode);
if (isComponent) {
this._currentDirectiveID.set(cmpOrDirective, parentID.concat([childIdx]));
this._componentTreeNode.set(cmpOrDirective, treeNode);
} else {
const elID = parentID.concat([childIdx]);
cmpOrDirective.forEach((dir: any) => {
this._currentDirectiveID.set(dir, elID);
this._componentTreeNode.set(dir, treeNode);
});
}
for (let i = childIdx + 1; i < siblingsArray.length; i++) {
const sibling = siblingsArray[i];
const siblingId = this._currentDirectiveID.get(sibling.component);
siblingId[siblingId.length - 1] = siblingId[siblingId.length - 1] + 1;
this._updateNestedNodeIds(sibling, siblingId.length - 1, 1);
}
}
delete(cmp: any): void {
const node = this._componentTreeNode.get(cmp);
const parent = node.parent;
let childrenArray = this._forest;
if (parent) {
childrenArray = parent.children;
}
const childIdx = childrenArray.indexOf(node);
childrenArray.splice(childIdx, 1);
for (let i = childIdx; i < childrenArray.length; i++) {
const sibling = childrenArray[i].component;
const siblingId = this._currentDirectiveID.get(sibling);
// We removed the sibling node, so we need to decrease the position
siblingId[siblingId.length - 1] = siblingId[siblingId.length - 1] - 1;
this._updateNestedNodeIds(childrenArray[i], siblingId.length - 1, -1);
}
}
index(root: IndexedNode, parent: TreeNode | null = null): void {
if (root.component) {
this._createdDirectives.add(root.component.instance);
const node = {
component: root.component.instance,
parent,
directives: [],
children: [],
};
this._componentTreeNode.set(root.component.instance, node);
if (parent) {
parent.children.push(node);
} else {
this._forest.push(node);
}
parent = node;
this._currentDirectiveID.set(root.component.instance, root.id);
root.directives.forEach(dir => {
this._currentDirectiveID.set(dir.instance, root.id);
});
}
root.children.forEach(child => this.index(child, parent));
}
hasDirective(dir: any) {
return this._createdDirectives.has(dir);
}
destroy() {
this._elementComponent = new Map<Node, any>();
this._currentDirectiveID = new Map<any, ElementID>();
this._createdDirectives = new Set<any>();
this._elementDirectives = new Map<Node, any[]>();
this._forest = [];
this._componentTreeNode = new Map<any, TreeNode>();
}
private _updateNestedNodeIds(p: TreeNode, level: number, incrementBy: number): void {
p.children.forEach(c => {
const id = this._currentDirectiveID.get(c.component);
id[level] = id[level] + incrementBy;
this._updateNestedNodeIds(c, level, incrementBy);
});
}
}
const getParentComponentFromDomNode = (node: Node, ng: DebuggingAPI) => {
let current = node;
let parent = null;
while (current.parentElement) {
current = current.parentElement;
const parentComponent = ng.getComponent(current);
if (parentComponent) {
parent = parentComponent;
break;
}
}
return parent;
};

View file

@ -1,23 +1,30 @@
import { ElementID, Node as ComponentNode } from 'protocol';
import { ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType, getComponentForest } from '../component-tree';
import { componentMetadata } from '../utils';
import { IdentityTracker } from './identity-tracker';
export type LifecyleHook =
| 'ngOnInit'
| 'ngOnDestroy'
| 'ngOnChanges'
| 'ngDoCheck'
| 'ngAfterContentInit'
| 'ngAfterContentChecked'
| 'ngAfterViewInit'
| 'ngAfterViewChecked';
export type CreationCallback = (component: any, id: ElementID) => void;
export type LifecycleCallback = (component: any, hook: LifecyleHook, duration: any) => void;
export type ChangeDetectionCallback = (component: any, id: ElementID, duration: number) => void;
export type DestroyCallback = (component: any, id: ElementID) => void;
interface TreeNode {
parent: TreeNode;
component: any;
children: TreeNode[];
}
declare const ng: any;
export interface Config {
onCreate: CreationCallback;
onDestroy: DestroyCallback;
onChangeDetection: ChangeDetectionCallback;
onLifecycleHook: LifecycleCallback;
}
/**
@ -30,13 +37,9 @@ export interface Config {
*/
export class ComponentTreeObserver {
private _mutationObserver = new MutationObserver(this._onMutation.bind(this));
private _elementComponent = new Map<Node, any>();
private _patched = new Map<any, () => void>();
private _currentComponentID = new Map<any, ElementID>();
private _lastChangeDetection = new Map<any, number>();
private _createdComponents = new Set<any>();
private _forest: TreeNode[] = [];
private _componentTreeNode = new Map<any, TreeNode>();
private _tracker = new IdentityTracker((window as any).ng);
constructor(private _config: Partial<Config>) {}
@ -54,12 +57,8 @@ export class ComponentTreeObserver {
destroy() {
this._mutationObserver.disconnect();
this._elementComponent = new Map<Node, any>();
this._currentComponentID = new Map<any, ElementID>();
this._lastChangeDetection = new Map<any, number>();
this._createdComponents = new Set<any>();
this._forest = [];
this._componentTreeNode = new Map<any, TreeNode>();
this._tracker.destroy();
for (const [cmp, template] of this._patched) {
const meta = componentMetadata(cmp);
@ -81,16 +80,11 @@ export class ComponentTreeObserver {
}
const component = ng.getComponent(node);
if (component) {
this._elementComponent.set(node, component);
if (this._config.onChangeDetection) {
this._observeComponent(component);
}
const parentComponent = getParentComponentFromDomNode(node);
this._updateInsertionID(component, parentComponent);
this._createdComponents.add(component);
this._tracker.insert(node, component);
this._fireCreationCallback(component);
@ -99,110 +93,67 @@ export class ComponentTreeObserver {
this._lastChangeDetection.delete(component);
}
}
let directives = [];
try {
directives = ng.getDirectives(node);
} catch {}
if (directives.length) {
this._tracker.insert(node, directives);
directives.forEach(dir => {
this._fireCreationCallback(dir);
});
}
}
private _fireCreationCallback(component): void {
const id = this._currentComponentID.get(component);
private _fireCreationCallback(component: any): void {
const id = this._tracker.getDirectiveID(component);
this._config.onCreate(component, id);
}
private _fireChangeDetectionCallback(component): void {
this._config.onChangeDetection(
component,
this._currentComponentID.get(component),
this._tracker.getDirectiveID(component),
this._lastChangeDetection.get(component)
);
}
private _onDeletedNodesMutation(node: Node): void {
const component = this._elementComponent.get(node);
if (!(node instanceof HTMLElement)) {
return;
}
const component = ng.getComponent(node);
if (component) {
this._elementComponent.delete(node);
this._updateDeletionID(component);
this._tracker.delete(component);
this._fireDestroyCallback(component);
}
let directives = [];
try {
directives = ng.getDirectives(node);
} catch {}
if (directives && directives.length) {
this._tracker.delete(directives[0]);
directives.forEach(dir => {
this._fireDestroyCallback(dir);
});
}
}
private _fireDestroyCallback(component): void {
const id = this._currentComponentID.get(component);
private _fireDestroyCallback(component: any): void {
const id = this._tracker.getDirectiveID(component);
this._config.onDestroy(component, id);
}
private _updateInsertionID(cmp: any, parent: any): void {
let parentTreeNode = null;
let parentID = [];
let childIdx = 0;
let siblingsArray = this._forest;
if (parent) {
const parentElement = ng.getHostElement(parent || document.documentElement);
const children = getComponentForest(parentElement)[0].children;
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < children.length; i++) {
if (children[i].component.instance === cmp) {
break;
}
if (this._componentTreeNode.has(children[i].component.instance)) {
childIdx++;
}
}
parentID = this._currentComponentID.get(parent);
parentTreeNode = this._componentTreeNode.get(parent);
siblingsArray = parentTreeNode.children;
}
const treeNode: TreeNode = {
parent: parentTreeNode,
children: [],
component: cmp,
};
siblingsArray.splice(childIdx, 0, treeNode);
this._currentComponentID.set(cmp, parentID.concat([childIdx]));
this._componentTreeNode.set(cmp, treeNode);
for (let i = childIdx + 1; i < siblingsArray.length; i++) {
const sibling = siblingsArray[i];
const siblingId = this._currentComponentID.get(sibling.component);
siblingId[siblingId.length - 1] = siblingId[siblingId.length - 1] + 1;
this._updateNestedNodeIds(sibling, siblingId.length - 1, 1);
}
}
private _updateDeletionID(cmp: any): void {
const node = this._componentTreeNode.get(cmp);
const parent = node.parent;
let childrenArray = this._forest;
if (parent) {
childrenArray = parent.children;
}
const childIdx = childrenArray.indexOf(node);
childrenArray.splice(childIdx, 1);
for (let i = childIdx; i < childrenArray.length; i++) {
const sibling = childrenArray[i].component;
const siblingId = this._currentComponentID.get(sibling);
// We removed the sibling node, so we need to decrease the position
siblingId[siblingId.length - 1] = siblingId[siblingId.length - 1] - 1;
this._updateNestedNodeIds(childrenArray[i], siblingId.length - 1, -1);
}
}
private _updateNestedNodeIds(p: TreeNode, level: number, incrementBy: number): void {
p.children.forEach(c => {
const id = this._currentComponentID.get(c.component);
id[level] = id[level] + incrementBy;
this._updateNestedNodeIds(c, level, incrementBy);
});
}
private _initializeChangeDetectionObserver(root: Element = document.documentElement): void {
if (!(root instanceof HTMLElement)) {
return;
}
const cmp = ng.getComponent(root);
if (cmp) {
this._elementComponent.set(root, cmp);
this._observeComponent(cmp);
}
// tslint:disable:prefer-for-of
@ -221,8 +172,8 @@ export class ComponentTreeObserver {
declarations.tView.template = function(_, component: any) {
const start = performance.now();
original.apply(this, arguments);
if (self._createdComponents.has(component)) {
self._config.onChangeDetection(component, self._currentComponentID.get(component), performance.now() - start);
if (self._tracker.hasDirective(component)) {
self._config.onChangeDetection(component, self._tracker.getDirectiveID(component), performance.now() - start);
} else {
self._lastChangeDetection.set(component, performance.now() - start);
}
@ -232,32 +183,14 @@ export class ComponentTreeObserver {
}
private _indexTree(): void {
const componentForest = indexForest(getComponentForest(document.documentElement));
componentForest.forEach(root => this._setIndexes(root));
}
private _setIndexes(root: IndexedNode, parent: TreeNode | null = null): void {
if (root.component) {
this._createdComponents.add(root.component.instance);
const node = {
component: root.component.instance,
parent,
children: [],
};
this._componentTreeNode.set(root.component.instance, node);
if (parent) {
parent.children.push(node);
} else {
this._forest.push(node);
}
parent = node;
this._currentComponentID.set(root.component.instance, root.id);
}
root.children.forEach(child => this._setIndexes(child, parent));
const componentForest = indexForest(getComponentForest(document.documentElement, (window as any).ng));
componentForest.forEach(root => this._tracker.index(root));
}
private _createOriginalTree(): void {
getComponentForest().forEach(root => this._fireInitialTreeCallbacks(root));
getComponentForest(document.documentElement, (window as any).ng).forEach(root =>
this._fireInitialTreeCallbacks(root)
);
}
private _fireInitialTreeCallbacks(root: ComponentTreeNode) {
@ -288,18 +221,4 @@ const indexTree = (node: ComponentNode, idx: number, parentId = []): IndexedNode
} as IndexedNode;
};
const getParentComponentFromDomNode = (node: Node) => {
let current = node;
let parent = null;
while (current.parentElement) {
current = current.parentElement;
const parentComponent = ng.getComponent(current);
if (parentComponent) {
parent = parentComponent;
break;
}
}
return parent;
};
export const indexForest = (forest: ComponentNode[]): IndexedNode[] => forest.map((n, i) => indexTree(n, i));