fast-tree-builder 1.0.2 → 2.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -83
- package/index.cjs +181 -174
- package/index.d.cts +109 -0
- package/index.d.mts +109 -0
- package/index.mjs +175 -0
- package/package.json +10 -10
- package/index.d.ts +0 -37
- package/index.js +0 -186
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
[](https://coveralls.io/github/lionel87/fast-tree-builder?branch=master)
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
`fast-tree-builder` is
|
|
9
|
+
`fast-tree-builder` is a high-performance TypeScript-first utility for constructing trees from iterable collections. It supports flexible input formats and fully customizable output node structures, enabling safe and idiomatic manipulation of hierarchical data.
|
|
10
|
+
|
|
10
11
|
|
|
11
12
|
## Prerequisites
|
|
12
13
|
|
|
@@ -14,44 +15,81 @@
|
|
|
14
15
|
- each item is identifiable by a unique id,
|
|
15
16
|
- the items are connected via a *parent id* OR *children ids*.
|
|
16
17
|
|
|
18
|
+
|
|
17
19
|
## Features
|
|
18
20
|
|
|
19
|
-
- **
|
|
21
|
+
- **Fully Typed and Customizable** – TypeScript support with correct types for the built tree.
|
|
22
|
+
- **Supports Both `parentId` and `childIds` Models** – Choose your relation style via options.
|
|
23
|
+
- **Iterable Input Support** – Works on arrays, sets, or any iterable.
|
|
24
|
+
- **Flexible Key Types** – Anything can be an identifier. Relations checked with (`childKey === parentKey`) comparison.
|
|
25
|
+
- **Fully Customizable Node Structure**: Design the node structure as you like.
|
|
26
|
+
- **O(n) Tree Construction** – Efficient building from unordered data, no sorting needed.
|
|
27
|
+
- **Bi-Directional Tree Links** – Nodes can store both `children` and `parent` references.
|
|
28
|
+
- **Multi-Root Support** – Handles disjoint trees naturally if no virtual root is present.
|
|
29
|
+
- **Map of Nodes** – Returned `Map` allows constant-time access to any node.
|
|
30
|
+
- **Tree Validation** – Detects cycles or nodes reachable through multiple paths.
|
|
31
|
+
- **Reference Validation** – Optionally enforce that all parent/child links are valid.
|
|
20
32
|
|
|
21
|
-
- **Bi-Directional Tree Traversal**: Pointers for parent and children both created for the nodes.
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
## Installation
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
```sh
|
|
37
|
+
npm install fast-tree-builder
|
|
38
|
+
```
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
or
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
```sh
|
|
43
|
+
yarn add fast-tree-builder
|
|
44
|
+
```
|
|
30
45
|
|
|
31
|
-
- **Flexible Key Types**: You can use any JavaScript value for identifying items. Relations checked with strict (`childKey === parentKey`) comparison.
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
## Documentation
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
### `buildTree(items: Iterable<T>, options: Options): TreeResult`
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
Builds a tree structure from an iterable list of items.
|
|
38
52
|
|
|
39
|
-
|
|
53
|
+
#### Parameters
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
* `items`: Any iterable of input items.
|
|
56
|
+
* `options`: Configuration object:
|
|
42
57
|
|
|
43
|
-
|
|
58
|
+
##### Required
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
npm install fast-tree-builder
|
|
47
|
-
```
|
|
60
|
+
- `id`: A key or function used to extract the unique identifier from each item.
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
##### One of:
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
- `parentId`: A key or function returning the parent ID of the item.
|
|
65
|
+
- `childIds`: A key or function returning an iterable of child IDs for the item.
|
|
66
|
+
|
|
67
|
+
##### Optional
|
|
68
|
+
|
|
69
|
+
- `nodeValueMapper`: Function to map an item to a custom value stored in the node. Optional.
|
|
70
|
+
- `nodeValueKey`: Key where the item's data is stored in the output node. Set to `false` to inline the item directly into the node. Default: `'value'`.
|
|
71
|
+
- `nodeParentKey`: Key where the node's parent reference is stored. Set to `false` to omit parent links. Default: `'parent'`.
|
|
72
|
+
- `nodeChildrenKey`: Key where the node's children are stored. Default: `'children'`.
|
|
73
|
+
- `withDepth`: When `true`, adds a `depth` property to each node indicating its depth in the tree. Also implies `validateTree`. Default: `false`.
|
|
74
|
+
- `validateReferences`: When `true`, verifies all `parentId` or `childIds` resolve to real items. Errors are thrown on invalid references. Default: `false`.
|
|
75
|
+
- `validateTree`: When `true`, verifies that the final structure is a valid tree (no cycles or multi-path references). Errors are thrown if the check fails. Default: `false`.
|
|
76
|
+
|
|
77
|
+
#### Returns
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
{
|
|
81
|
+
roots: TreeNode[], // top-level nodes
|
|
82
|
+
nodes: Map<id, TreeNode> // all nodes by id
|
|
83
|
+
}
|
|
53
84
|
```
|
|
54
85
|
|
|
86
|
+
#### Throws
|
|
87
|
+
|
|
88
|
+
- Missing required `id`, `parentId`/`childIds`, or `options` parameter
|
|
89
|
+
- Duplicate item identifiers
|
|
90
|
+
- Invalid reference (if `validateReferences` is enabled)
|
|
91
|
+
- Cycle or structural error (if `validateTree` or `withDepth` is enabled)
|
|
92
|
+
|
|
55
93
|
|
|
56
94
|
## Usage
|
|
57
95
|
|
|
@@ -74,46 +112,46 @@ const items = [
|
|
|
74
112
|
|
|
75
113
|
const { roots, nodes } = buildTree(items, {
|
|
76
114
|
// the input items:
|
|
77
|
-
|
|
78
|
-
|
|
115
|
+
id: 'id',
|
|
116
|
+
parentId: 'parent',
|
|
79
117
|
// the built node:
|
|
80
|
-
|
|
118
|
+
nodeValueKey: 'value',
|
|
81
119
|
nodeParentKey: 'parent',
|
|
82
120
|
nodeChildrenKey: 'children',
|
|
83
121
|
});
|
|
84
122
|
|
|
85
|
-
console.log(roots[0].
|
|
123
|
+
console.log(roots[0].value.name);
|
|
86
124
|
// Expected output: Root 1
|
|
87
125
|
|
|
88
|
-
console.log(roots[0].children[1].
|
|
126
|
+
console.log(roots[0].children[1].value.name);
|
|
89
127
|
// Expected output: Child 1.2
|
|
90
128
|
|
|
91
|
-
console.log(roots[0].children[1].parent.
|
|
129
|
+
console.log(roots[0].children[1].parent.value.name);
|
|
92
130
|
// Expected output: Root 1
|
|
93
131
|
|
|
94
132
|
console.log(roots);
|
|
95
133
|
// Expected output: [
|
|
96
|
-
// {
|
|
97
|
-
// {
|
|
98
|
-
// {
|
|
134
|
+
// { value: { id: 1, parent: null, name: 'Root 1' }, children: [
|
|
135
|
+
// { value: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
|
|
136
|
+
// { value: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } }
|
|
99
137
|
// ] },
|
|
100
|
-
// {
|
|
101
|
-
// {
|
|
138
|
+
// { value: { id: 2, parent: null, name: 'Root 2' }, children: [
|
|
139
|
+
// { value: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
|
|
102
140
|
// ] }
|
|
103
141
|
// ]
|
|
104
142
|
|
|
105
143
|
console.log(nodes);
|
|
106
144
|
// Expected output: Map {
|
|
107
|
-
// 1 => {
|
|
108
|
-
// {
|
|
109
|
-
// {
|
|
145
|
+
// 1 => { value: { id: 1, parent: null, name: 'Root 1' }, children: [
|
|
146
|
+
// { value: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
|
|
147
|
+
// { value: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } }
|
|
110
148
|
// ] },
|
|
111
|
-
// 2 => {
|
|
112
|
-
// {
|
|
149
|
+
// 2 => { value: { id: 2, parent: null, name: 'Root 2' }, children: [
|
|
150
|
+
// { value: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
|
|
113
151
|
// ] },
|
|
114
|
-
// 3 => {
|
|
115
|
-
// 4 => {
|
|
116
|
-
// 5 => {
|
|
152
|
+
// 3 => { value: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
|
|
153
|
+
// 4 => { value: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } },
|
|
154
|
+
// 5 => { value: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
|
|
117
155
|
// }
|
|
118
156
|
```
|
|
119
157
|
|
|
@@ -132,7 +170,8 @@ const items = [
|
|
|
132
170
|
];
|
|
133
171
|
|
|
134
172
|
const { roots, nodes } = buildTree(items, {
|
|
135
|
-
|
|
173
|
+
id: 'id',
|
|
174
|
+
childIds: 'children',
|
|
136
175
|
});
|
|
137
176
|
```
|
|
138
177
|
|
|
@@ -152,12 +191,12 @@ const items = [
|
|
|
152
191
|
];
|
|
153
192
|
|
|
154
193
|
const { roots, nodes } = buildTree(items, {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
194
|
+
id: item => item.key?.n,
|
|
195
|
+
parentId: item => item.parentKey?.n,
|
|
196
|
+
nodeValueMapper: item => ({ title: item.name }),
|
|
197
|
+
nodeValueKey: false, // merge item data into node
|
|
158
198
|
nodeParentKey: 'up',
|
|
159
199
|
nodeChildrenKey: 'down',
|
|
160
|
-
mapNodeData(item) { return { title: item.name }; },
|
|
161
200
|
});
|
|
162
201
|
|
|
163
202
|
console.log(roots[0].title);
|
|
@@ -210,10 +249,10 @@ const items = [
|
|
|
210
249
|
];
|
|
211
250
|
|
|
212
251
|
const { roots, nodes } = buildTree(items, {
|
|
213
|
-
|
|
214
|
-
parentKey
|
|
215
|
-
|
|
216
|
-
|
|
252
|
+
id: item => item.substring(2, 4),
|
|
253
|
+
parentKey: item => item.substring(0, 2),
|
|
254
|
+
nodeValueMapper: item => ({ name: item.substring(4) }),
|
|
255
|
+
nodeValueKey: false, // merge item data into node
|
|
217
256
|
});
|
|
218
257
|
|
|
219
258
|
console.log(roots[0].name);
|
|
@@ -234,40 +273,6 @@ console.log(roots);
|
|
|
234
273
|
// ]
|
|
235
274
|
```
|
|
236
275
|
|
|
237
|
-
## Documentation
|
|
238
|
-
|
|
239
|
-
### `buildTree(items: Iterable<T>, options: BuildTreeOptions): TreeResult<T>`
|
|
240
|
-
|
|
241
|
-
Builds a tree from the given iterable `items` using the specified `options`.
|
|
242
|
-
|
|
243
|
-
Parameters
|
|
244
|
-
|
|
245
|
-
- `items`: An iterable data structure containing the items of the tree.
|
|
246
|
-
- `options`: An object specifying the build options. It has the following properties:
|
|
247
|
-
- `mode`: (Optional) Defines the item connection method. `children` means items defines their children in an array, child nodes connects to these; `parent` means items defines their parent, parent nodes connects to these. Defaults to `parent`.
|
|
248
|
-
- `key`: (Optional) The key used to identify items. It can be a string, number, symbol, or a function that extracts the key from an item. Defaults to `'id'`.
|
|
249
|
-
- `parentKey`: (Optional) The key used to identify the parent of each item. It can be a `string`, `number`, `symbol`, or a `function` that extracts the parent key from an item. Defaults to `'parent'`.
|
|
250
|
-
- `nodeDataKey`: (Optional) The key used to store the item's data in each node. It can be a `string`, `number`, `symbol`, or `false` if the data should be merged directly into the node. Defaults to `'data'`.
|
|
251
|
-
- `nodeParentKey`: (Optional) The key used to store the parent node in each node. It can be a `string`, `number`, `symbol`, or `false` if the parent node should not be included. Defaults to `'parent'`.
|
|
252
|
-
- `nodeChildrenKey`: (Optional) The key used to store the children nodes in each node. It can be a `string`, `number`, `symbol`. Defaults to `'children'`.
|
|
253
|
-
- `mapNodeData`: (Optional) A function that maps an item to its corresponding node data. It allows transforming the item before assigning it to the node. Defaults to `undefined`.
|
|
254
|
-
- `validRootKeys`: (Optional) An iterable containing key values that can be accepted as root nodes. If provided, any item with a key not present in this iterable will cause an error to be thrown. Defaults to `undefined`.
|
|
255
|
-
- `validRootParentKeys`: (Optional) Only available when `mode` is set to `parent`. An iterable containing key values that can be accepted the parent field values of root nodes. If provided, any root node with a parent key not present in this iterable will cause an error to be thrown. Defaults to `undefined`.
|
|
256
|
-
- `validateTree`: (Optional) A boolean flag that determines whether to validate the resulting data structure. If the structure is a cyclic graph, an `Error` will be thrown. Requires additional `O(n)` time to compute. Defaults to `false`.
|
|
257
|
-
|
|
258
|
-
Returns
|
|
259
|
-
|
|
260
|
-
An object with the following properties:
|
|
261
|
-
|
|
262
|
-
- `roots`: An array of the root nodes of the built tree.
|
|
263
|
-
- `nodes`: A `Map` object containing all nodes of the built tree, with keys corresponding to their identifiers.
|
|
264
|
-
|
|
265
|
-
Throws `Error` when:
|
|
266
|
-
|
|
267
|
-
- A duplicate identifier is recieved,
|
|
268
|
-
- or `validRootKeys` is set and an invalid key is recieved,
|
|
269
|
-
- or `validRootParentKeys` is set and an invalid parent key is recieved,
|
|
270
|
-
- or `validateTree` is set to `true` and a cyclic graph is the result.
|
|
271
276
|
|
|
272
277
|
## FAQ
|
|
273
278
|
|
|
@@ -291,7 +296,7 @@ for (const item of items) {
|
|
|
291
296
|
node = {};
|
|
292
297
|
nodes.set(item.id, node);
|
|
293
298
|
}
|
|
294
|
-
node.
|
|
299
|
+
node.value = item; // Or Object.assign(node, item);
|
|
295
300
|
if (item.parentId) {
|
|
296
301
|
let parent = nodes.get(item.parentId);
|
|
297
302
|
if (!parent) {
|
package/index.cjs
CHANGED
|
@@ -1,188 +1,195 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var index_exports = {};
|
|
20
|
+
__export(index_exports, {
|
|
21
|
+
default: () => buildTree
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(index_exports);
|
|
24
|
+
function buildTree(items, options) {
|
|
25
|
+
if (!options) {
|
|
26
|
+
throw new Error('Missing required "options" parameter.');
|
|
27
|
+
}
|
|
28
|
+
const {
|
|
29
|
+
id: idAccessor,
|
|
30
|
+
parentId: parentIdAccessor,
|
|
31
|
+
childIds: childIdsAccessor,
|
|
32
|
+
nodeValueMapper,
|
|
33
|
+
nodeValueKey = "value",
|
|
34
|
+
nodeParentKey = "parent",
|
|
35
|
+
nodeChildrenKey = "children",
|
|
36
|
+
withDepth = false,
|
|
37
|
+
validateTree = false,
|
|
38
|
+
validateReferences = false
|
|
39
|
+
} = options;
|
|
40
|
+
if (!idAccessor) {
|
|
41
|
+
throw new Error('Option "id" is required.');
|
|
42
|
+
}
|
|
43
|
+
if (!parentIdAccessor && !childIdsAccessor) {
|
|
44
|
+
throw new Error('Either "parentId" or "childIds" must be provided.');
|
|
45
|
+
}
|
|
46
|
+
if (parentIdAccessor && childIdsAccessor) {
|
|
47
|
+
throw new Error('"parentId" and "childIds" cannot be used together.');
|
|
48
|
+
}
|
|
49
|
+
const roots = [];
|
|
50
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
51
|
+
if (parentIdAccessor) {
|
|
52
|
+
const waitingForParent = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
55
|
+
if (nodes.has(id)) {
|
|
56
|
+
throw new Error(`Duplicate identifier "${id}".`);
|
|
57
|
+
}
|
|
58
|
+
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
59
|
+
nodes.set(id, node);
|
|
60
|
+
const parentId = typeof parentIdAccessor === "function" ? parentIdAccessor(item) : item[parentIdAccessor];
|
|
61
|
+
const parentNode = nodes.get(parentId);
|
|
62
|
+
if (parentNode) {
|
|
63
|
+
parentNode[nodeChildrenKey] ||= [];
|
|
64
|
+
parentNode[nodeChildrenKey].push(node);
|
|
65
|
+
if (nodeParentKey !== false) {
|
|
66
|
+
node[nodeParentKey] = parentNode;
|
|
50
67
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
for (const root of node[nodeChildrenKey]) {
|
|
59
|
-
// Root nodes does not have a parent, unlink the dangling node
|
|
60
|
-
if (nodeParentKey !== false) {
|
|
61
|
-
delete root[nodeParentKey];
|
|
62
|
-
}
|
|
63
|
-
roots.push(root);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
68
|
+
} else {
|
|
69
|
+
const siblings = waitingForParent.get(parentId);
|
|
70
|
+
if (siblings) {
|
|
71
|
+
siblings.push(node);
|
|
72
|
+
} else {
|
|
73
|
+
waitingForParent.set(parentId, [node]);
|
|
66
74
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// TODO: this could be optimized
|
|
79
|
-
if (validRootKeys) {
|
|
80
|
-
const rootsSet = new Set(roots);
|
|
81
|
-
const validKeys = new Set(validRootKeys);
|
|
82
|
-
for (const [key, node] of nodes) {
|
|
83
|
-
if (rootsSet.has(node) && !validKeys.has(key)) {
|
|
84
|
-
throw new Error(`A root node has an invalid key "${key}"`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
75
|
+
}
|
|
76
|
+
const children = waitingForParent.get(id);
|
|
77
|
+
if (children) {
|
|
78
|
+
node[nodeChildrenKey] = children;
|
|
79
|
+
if (nodeParentKey !== false) {
|
|
80
|
+
for (const child of children) {
|
|
81
|
+
child[nodeParentKey] = node;
|
|
82
|
+
}
|
|
87
83
|
}
|
|
84
|
+
waitingForParent.delete(id);
|
|
85
|
+
}
|
|
88
86
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
node[nodeDataKey] = nodeData;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
Object.assign(node, nodeData);
|
|
115
|
-
}
|
|
116
|
-
// Link children to this node
|
|
117
|
-
if (keyOfChildNodes) {
|
|
118
|
-
node[nodeChildrenKey] = [];
|
|
119
|
-
for (const keyOfChildNode of keyOfChildNodes) {
|
|
120
|
-
let childNode = danglingNodes.get(keyOfChildNode);
|
|
121
|
-
if (childNode) {
|
|
122
|
-
nodes.set(keyOfChildNode, childNode);
|
|
123
|
-
danglingNodes.delete(keyOfChildNode);
|
|
124
|
-
// Set the parent on child node
|
|
125
|
-
if (nodeParentKey !== false) {
|
|
126
|
-
childNode[nodeParentKey] = node;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
else if (nodes.has(keyOfChildNode)) {
|
|
130
|
-
throw new Error(`Duplicate parent detected for "${keyOfChildNode}"`);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
// We create a new temporary node
|
|
134
|
-
childNode = {};
|
|
135
|
-
// Set the parent on child node
|
|
136
|
-
if (nodeParentKey !== false) {
|
|
137
|
-
childNode[nodeParentKey] = node;
|
|
138
|
-
}
|
|
139
|
-
nodes.set(keyOfChildNode, childNode);
|
|
140
|
-
incompleteNodes.add(keyOfChildNode);
|
|
141
|
-
}
|
|
142
|
-
node[nodeChildrenKey].push(childNode);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
87
|
+
for (const [parentId, nodes2] of waitingForParent.entries()) {
|
|
88
|
+
if (validateReferences && parentId != null) {
|
|
89
|
+
throw new Error(`Referential integrity violation: parentId "${parentId}" does not match any item in the input.`);
|
|
90
|
+
}
|
|
91
|
+
for (const node of nodes2) {
|
|
92
|
+
roots.push(node);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else if (childIdsAccessor) {
|
|
96
|
+
const waitingChildren = /* @__PURE__ */ new Map();
|
|
97
|
+
const rootCandidates = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
100
|
+
if (nodes.has(id)) {
|
|
101
|
+
throw new Error(`Duplicate identifier "${id}".`);
|
|
102
|
+
}
|
|
103
|
+
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
104
|
+
nodes.set(id, node);
|
|
105
|
+
const childIds = typeof childIdsAccessor === "function" ? childIdsAccessor(item) : item[childIdsAccessor];
|
|
106
|
+
if (childIds != null) {
|
|
107
|
+
if (typeof childIds[Symbol.iterator] !== "function") {
|
|
108
|
+
throw new Error(`Item "${id}" has invalid children: expected an iterable value.`);
|
|
145
109
|
}
|
|
146
|
-
|
|
147
|
-
|
|
110
|
+
node[nodeChildrenKey] = [];
|
|
111
|
+
for (const childId of childIds) {
|
|
112
|
+
const childNode = nodes.get(childId);
|
|
113
|
+
if (childNode) {
|
|
114
|
+
node[nodeChildrenKey].push(childNode);
|
|
115
|
+
if (nodeParentKey !== false) {
|
|
116
|
+
if (childNode[nodeParentKey] && childNode[nodeParentKey] !== node) {
|
|
117
|
+
throw new Error(`Node "${childId}" already has a different parent, refusing to overwrite. Set "nodeParentKey" to false to supress this error.`);
|
|
118
|
+
}
|
|
119
|
+
childNode[nodeParentKey] = node;
|
|
120
|
+
}
|
|
121
|
+
rootCandidates.delete(childId);
|
|
122
|
+
} else {
|
|
123
|
+
waitingChildren.set(childId, {
|
|
124
|
+
parentNode: node,
|
|
125
|
+
childIndex: node[nodeChildrenKey].length
|
|
126
|
+
});
|
|
127
|
+
node[nodeChildrenKey].push(null);
|
|
128
|
+
}
|
|
148
129
|
}
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
for (const [key, node] of danglingNodes.entries()) {
|
|
152
|
-
if (!validKeys.has(key)) {
|
|
153
|
-
throw new Error(`A root node has an invalid key "${key}"`);
|
|
154
|
-
}
|
|
155
|
-
roots.push(node);
|
|
156
|
-
nodes.set(key, node);
|
|
157
|
-
}
|
|
130
|
+
if (node[nodeChildrenKey].length === 0) {
|
|
131
|
+
delete node[nodeChildrenKey];
|
|
158
132
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
133
|
+
}
|
|
134
|
+
const parentDescriptor = waitingChildren.get(id);
|
|
135
|
+
if (parentDescriptor) {
|
|
136
|
+
const { parentNode, childIndex } = parentDescriptor;
|
|
137
|
+
parentNode[nodeChildrenKey][childIndex] = node;
|
|
138
|
+
if (nodeParentKey !== false) {
|
|
139
|
+
if (node[nodeParentKey] && node[nodeParentKey] !== parentNode) {
|
|
140
|
+
throw new Error(`Node "${id}" already has a different parent, refusing to overwrite. Set "nodeParentKey" to false to supress this error.`);
|
|
141
|
+
}
|
|
142
|
+
node[nodeParentKey] = parentNode;
|
|
164
143
|
}
|
|
144
|
+
waitingChildren.delete(id);
|
|
145
|
+
} else {
|
|
146
|
+
rootCandidates.set(id, node);
|
|
147
|
+
}
|
|
165
148
|
}
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
for (const child of node[nodeChildrenKey]) {
|
|
178
|
-
gray.push(child);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
149
|
+
if (waitingChildren.size > 0) {
|
|
150
|
+
if (validateReferences) {
|
|
151
|
+
const childId = waitingChildren.keys().next().value;
|
|
152
|
+
throw new Error(`Referential integrity violation: child reference "${childId}" does not match any item in the input.`);
|
|
153
|
+
}
|
|
154
|
+
const pending = Array.from(waitingChildren.values());
|
|
155
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
156
|
+
const { parentNode, childIndex } = pending[i];
|
|
157
|
+
parentNode[nodeChildrenKey].splice(childIndex, 1);
|
|
158
|
+
if (parentNode[nodeChildrenKey].length === 0) {
|
|
159
|
+
delete parentNode[nodeChildrenKey];
|
|
181
160
|
}
|
|
182
|
-
|
|
183
|
-
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const node of rootCandidates.values()) {
|
|
164
|
+
roots.push(node);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (validateTree || withDepth) {
|
|
168
|
+
if (roots.length === 0 && nodes.size > 0) {
|
|
169
|
+
throw new Error("Tree validation failed: detected a cycle.");
|
|
170
|
+
}
|
|
171
|
+
const stack = [...roots].map((node) => ({ node, depth: 0 }));
|
|
172
|
+
const visited = /* @__PURE__ */ new Set();
|
|
173
|
+
let processedCount = 0;
|
|
174
|
+
const MAX_NODES = nodes.size;
|
|
175
|
+
while (stack.length > 0 && processedCount++ <= MAX_NODES) {
|
|
176
|
+
const { node, depth } = stack.pop();
|
|
177
|
+
if (visited.has(node)) {
|
|
178
|
+
throw new Error("Tree validation failed: a node reachable via multiple paths.");
|
|
179
|
+
}
|
|
180
|
+
visited.add(node);
|
|
181
|
+
if (withDepth) {
|
|
182
|
+
node.depth = depth;
|
|
183
|
+
}
|
|
184
|
+
if (node[nodeChildrenKey]) {
|
|
185
|
+
for (const child of node[nodeChildrenKey]) {
|
|
186
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
184
187
|
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (nodes.size !== visited.size) {
|
|
191
|
+
throw new Error("Tree validation failed: detected a cycle.");
|
|
185
192
|
}
|
|
186
|
-
|
|
193
|
+
}
|
|
194
|
+
return { roots, nodes };
|
|
187
195
|
}
|
|
188
|
-
exports.default = buildTree;
|
package/index.d.cts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
type TreeNode<TValue, TValueKey extends string | number | symbol | false, TParentKey extends string | number | symbol | false, TChildrenKey extends string | number | symbol, TWithDepth extends boolean> = (TValueKey extends false ? (TParentKey extends false ? Omit<TValue, TChildrenKey> & {
|
|
2
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
3
|
+
} : Omit<TValue, Exclude<TParentKey, false> | TChildrenKey> & {
|
|
4
|
+
[k in Exclude<TParentKey, false>]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>;
|
|
5
|
+
} & {
|
|
6
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
7
|
+
}) : (TParentKey extends false ? {
|
|
8
|
+
[k in Exclude<TValueKey, false>]: TValue;
|
|
9
|
+
} & {
|
|
10
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
11
|
+
} : {
|
|
12
|
+
[k in Exclude<TValueKey, false>]: TValue;
|
|
13
|
+
} & {
|
|
14
|
+
[k in Exclude<TParentKey, false>]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>;
|
|
15
|
+
} & {
|
|
16
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
17
|
+
})) & (TWithDepth extends true ? {
|
|
18
|
+
depth: number;
|
|
19
|
+
} : {});
|
|
20
|
+
type AccessorReturnType<T, P extends (keyof T) | ((item: T) => any)> = P extends ((item: T) => infer R) ? R : P extends (keyof T) ? T[P] : never;
|
|
21
|
+
type IterableKeys<T> = {
|
|
22
|
+
[K in keyof T]: T[K] extends Iterable<unknown> ? K : never;
|
|
23
|
+
}[keyof T];
|
|
24
|
+
export default function buildTree<TInputValue extends (TIdAccessor extends (keyof TInputValue) ? object : (TNodeValueKey extends false ? object : unknown)), TIdAccessor extends (keyof TInputValue) | ((item: TInputValue) => unknown), TMappedValue extends (TNodeValueKey extends false ? object : unknown) = TInputValue, TNodeValueKey extends string | number | symbol | false = 'value', TNodeParentKey extends string | number | symbol | false = 'parent', TNodeChildrenKey extends string | number | symbol = 'children', TWithDepth extends boolean = false, TTreeNode = TreeNode<TMappedValue extends undefined ? TInputValue : TMappedValue, TNodeValueKey, TNodeParentKey, TNodeChildrenKey, TWithDepth>>(items: Iterable<TInputValue>, options: {
|
|
25
|
+
/**
|
|
26
|
+
* A string key or function used to get the item's unique identifier.
|
|
27
|
+
*/
|
|
28
|
+
id: TIdAccessor;
|
|
29
|
+
/**
|
|
30
|
+
* A string key or function used to get the item's parent identifier.
|
|
31
|
+
*
|
|
32
|
+
* Either `parentId` or `childIds` must be provided.
|
|
33
|
+
*/
|
|
34
|
+
parentId?: (keyof TInputValue) | ((item: TInputValue) => unknown);
|
|
35
|
+
/**
|
|
36
|
+
* A string key or function to retrieve a list of child identifiers from an item.
|
|
37
|
+
*
|
|
38
|
+
* Either `parentId` or `childIds` must be provided.
|
|
39
|
+
*/
|
|
40
|
+
childIds?: IterableKeys<TInputValue> | ((item: TInputValue) => Iterable<unknown> | null | undefined);
|
|
41
|
+
/**
|
|
42
|
+
* Maps the input item to a different value stored in the resulting tree node.
|
|
43
|
+
*/
|
|
44
|
+
nodeValueMapper?: {
|
|
45
|
+
(item: TInputValue): TMappedValue;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Object key used to store the mapped value in the output tree node.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to `'value'`.
|
|
51
|
+
*/
|
|
52
|
+
nodeValueKey?: TNodeValueKey;
|
|
53
|
+
/**
|
|
54
|
+
* Object key used to store the reference to the parent node in the output tree node.
|
|
55
|
+
*
|
|
56
|
+
* Defaults to `'parent'`.
|
|
57
|
+
*/
|
|
58
|
+
nodeParentKey?: TNodeParentKey;
|
|
59
|
+
/**
|
|
60
|
+
* Object key used to store the list of child nodes in the output tree node.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to `'children'`.
|
|
63
|
+
*/
|
|
64
|
+
nodeChildrenKey?: TNodeChildrenKey;
|
|
65
|
+
/**
|
|
66
|
+
* When enabled, adds a `depth` property to each output node indicating its depth within the tree.
|
|
67
|
+
* Root nodes have a depth of 0.
|
|
68
|
+
*
|
|
69
|
+
* This also implicitly enables `validateTree`, as depth assignment requires a valid tree structure,
|
|
70
|
+
* and both operations share the same traversal logic.
|
|
71
|
+
*
|
|
72
|
+
* Defaults to `false`.
|
|
73
|
+
*/
|
|
74
|
+
withDepth?: TWithDepth;
|
|
75
|
+
/**
|
|
76
|
+
* Validates that the final structure forms a tree.
|
|
77
|
+
*
|
|
78
|
+
* Ensures:
|
|
79
|
+
* - No cycles
|
|
80
|
+
* - No node reachable through multiple paths
|
|
81
|
+
*
|
|
82
|
+
* Throws if the structure is not a proper tree.
|
|
83
|
+
*
|
|
84
|
+
* Defaults to `false`
|
|
85
|
+
*/
|
|
86
|
+
validateTree?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Validates referential integrity of the input.
|
|
89
|
+
*
|
|
90
|
+
* In strict mode:
|
|
91
|
+
* - All parent and child references must point to existing items in the input.
|
|
92
|
+
* - Root items must have their `parentId` unset, `null`, or `undefined`.
|
|
93
|
+
*
|
|
94
|
+
* Any invalid or missing references will result in an error during tree construction.
|
|
95
|
+
*
|
|
96
|
+
* Defaults to `false`
|
|
97
|
+
*/
|
|
98
|
+
validateReferences?: boolean;
|
|
99
|
+
} & ({
|
|
100
|
+
parentId?: never;
|
|
101
|
+
childIds: IterableKeys<TInputValue> | ((item: TInputValue) => unknown[] | null | undefined);
|
|
102
|
+
} | {
|
|
103
|
+
parentId: (keyof TInputValue) | ((item: TInputValue) => unknown);
|
|
104
|
+
childIds?: never;
|
|
105
|
+
})): {
|
|
106
|
+
roots: TTreeNode[];
|
|
107
|
+
nodes: Map<AccessorReturnType<TInputValue, TIdAccessor>, TTreeNode>;
|
|
108
|
+
};
|
|
109
|
+
export {};
|
package/index.d.mts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
type TreeNode<TValue, TValueKey extends string | number | symbol | false, TParentKey extends string | number | symbol | false, TChildrenKey extends string | number | symbol, TWithDepth extends boolean> = (TValueKey extends false ? (TParentKey extends false ? Omit<TValue, TChildrenKey> & {
|
|
2
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
3
|
+
} : Omit<TValue, Exclude<TParentKey, false> | TChildrenKey> & {
|
|
4
|
+
[k in Exclude<TParentKey, false>]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>;
|
|
5
|
+
} & {
|
|
6
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
7
|
+
}) : (TParentKey extends false ? {
|
|
8
|
+
[k in Exclude<TValueKey, false>]: TValue;
|
|
9
|
+
} & {
|
|
10
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
11
|
+
} : {
|
|
12
|
+
[k in Exclude<TValueKey, false>]: TValue;
|
|
13
|
+
} & {
|
|
14
|
+
[k in Exclude<TParentKey, false>]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>;
|
|
15
|
+
} & {
|
|
16
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
17
|
+
})) & (TWithDepth extends true ? {
|
|
18
|
+
depth: number;
|
|
19
|
+
} : {});
|
|
20
|
+
type AccessorReturnType<T, P extends (keyof T) | ((item: T) => any)> = P extends ((item: T) => infer R) ? R : P extends (keyof T) ? T[P] : never;
|
|
21
|
+
type IterableKeys<T> = {
|
|
22
|
+
[K in keyof T]: T[K] extends Iterable<unknown> ? K : never;
|
|
23
|
+
}[keyof T];
|
|
24
|
+
export default function buildTree<TInputValue extends (TIdAccessor extends (keyof TInputValue) ? object : (TNodeValueKey extends false ? object : unknown)), TIdAccessor extends (keyof TInputValue) | ((item: TInputValue) => unknown), TMappedValue extends (TNodeValueKey extends false ? object : unknown) = TInputValue, TNodeValueKey extends string | number | symbol | false = 'value', TNodeParentKey extends string | number | symbol | false = 'parent', TNodeChildrenKey extends string | number | symbol = 'children', TWithDepth extends boolean = false, TTreeNode = TreeNode<TMappedValue extends undefined ? TInputValue : TMappedValue, TNodeValueKey, TNodeParentKey, TNodeChildrenKey, TWithDepth>>(items: Iterable<TInputValue>, options: {
|
|
25
|
+
/**
|
|
26
|
+
* A string key or function used to get the item's unique identifier.
|
|
27
|
+
*/
|
|
28
|
+
id: TIdAccessor;
|
|
29
|
+
/**
|
|
30
|
+
* A string key or function used to get the item's parent identifier.
|
|
31
|
+
*
|
|
32
|
+
* Either `parentId` or `childIds` must be provided.
|
|
33
|
+
*/
|
|
34
|
+
parentId?: (keyof TInputValue) | ((item: TInputValue) => unknown);
|
|
35
|
+
/**
|
|
36
|
+
* A string key or function to retrieve a list of child identifiers from an item.
|
|
37
|
+
*
|
|
38
|
+
* Either `parentId` or `childIds` must be provided.
|
|
39
|
+
*/
|
|
40
|
+
childIds?: IterableKeys<TInputValue> | ((item: TInputValue) => Iterable<unknown> | null | undefined);
|
|
41
|
+
/**
|
|
42
|
+
* Maps the input item to a different value stored in the resulting tree node.
|
|
43
|
+
*/
|
|
44
|
+
nodeValueMapper?: {
|
|
45
|
+
(item: TInputValue): TMappedValue;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Object key used to store the mapped value in the output tree node.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to `'value'`.
|
|
51
|
+
*/
|
|
52
|
+
nodeValueKey?: TNodeValueKey;
|
|
53
|
+
/**
|
|
54
|
+
* Object key used to store the reference to the parent node in the output tree node.
|
|
55
|
+
*
|
|
56
|
+
* Defaults to `'parent'`.
|
|
57
|
+
*/
|
|
58
|
+
nodeParentKey?: TNodeParentKey;
|
|
59
|
+
/**
|
|
60
|
+
* Object key used to store the list of child nodes in the output tree node.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to `'children'`.
|
|
63
|
+
*/
|
|
64
|
+
nodeChildrenKey?: TNodeChildrenKey;
|
|
65
|
+
/**
|
|
66
|
+
* When enabled, adds a `depth` property to each output node indicating its depth within the tree.
|
|
67
|
+
* Root nodes have a depth of 0.
|
|
68
|
+
*
|
|
69
|
+
* This also implicitly enables `validateTree`, as depth assignment requires a valid tree structure,
|
|
70
|
+
* and both operations share the same traversal logic.
|
|
71
|
+
*
|
|
72
|
+
* Defaults to `false`.
|
|
73
|
+
*/
|
|
74
|
+
withDepth?: TWithDepth;
|
|
75
|
+
/**
|
|
76
|
+
* Validates that the final structure forms a tree.
|
|
77
|
+
*
|
|
78
|
+
* Ensures:
|
|
79
|
+
* - No cycles
|
|
80
|
+
* - No node reachable through multiple paths
|
|
81
|
+
*
|
|
82
|
+
* Throws if the structure is not a proper tree.
|
|
83
|
+
*
|
|
84
|
+
* Defaults to `false`
|
|
85
|
+
*/
|
|
86
|
+
validateTree?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Validates referential integrity of the input.
|
|
89
|
+
*
|
|
90
|
+
* In strict mode:
|
|
91
|
+
* - All parent and child references must point to existing items in the input.
|
|
92
|
+
* - Root items must have their `parentId` unset, `null`, or `undefined`.
|
|
93
|
+
*
|
|
94
|
+
* Any invalid or missing references will result in an error during tree construction.
|
|
95
|
+
*
|
|
96
|
+
* Defaults to `false`
|
|
97
|
+
*/
|
|
98
|
+
validateReferences?: boolean;
|
|
99
|
+
} & ({
|
|
100
|
+
parentId?: never;
|
|
101
|
+
childIds: IterableKeys<TInputValue> | ((item: TInputValue) => unknown[] | null | undefined);
|
|
102
|
+
} | {
|
|
103
|
+
parentId: (keyof TInputValue) | ((item: TInputValue) => unknown);
|
|
104
|
+
childIds?: never;
|
|
105
|
+
})): {
|
|
106
|
+
roots: TTreeNode[];
|
|
107
|
+
nodes: Map<AccessorReturnType<TInputValue, TIdAccessor>, TTreeNode>;
|
|
108
|
+
};
|
|
109
|
+
export {};
|
package/index.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
function buildTree(items, options) {
|
|
2
|
+
if (!options) {
|
|
3
|
+
throw new Error('Missing required "options" parameter.');
|
|
4
|
+
}
|
|
5
|
+
const {
|
|
6
|
+
id: idAccessor,
|
|
7
|
+
parentId: parentIdAccessor,
|
|
8
|
+
childIds: childIdsAccessor,
|
|
9
|
+
nodeValueMapper,
|
|
10
|
+
nodeValueKey = "value",
|
|
11
|
+
nodeParentKey = "parent",
|
|
12
|
+
nodeChildrenKey = "children",
|
|
13
|
+
withDepth = false,
|
|
14
|
+
validateTree = false,
|
|
15
|
+
validateReferences = false
|
|
16
|
+
} = options;
|
|
17
|
+
if (!idAccessor) {
|
|
18
|
+
throw new Error('Option "id" is required.');
|
|
19
|
+
}
|
|
20
|
+
if (!parentIdAccessor && !childIdsAccessor) {
|
|
21
|
+
throw new Error('Either "parentId" or "childIds" must be provided.');
|
|
22
|
+
}
|
|
23
|
+
if (parentIdAccessor && childIdsAccessor) {
|
|
24
|
+
throw new Error('"parentId" and "childIds" cannot be used together.');
|
|
25
|
+
}
|
|
26
|
+
const roots = [];
|
|
27
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
28
|
+
if (parentIdAccessor) {
|
|
29
|
+
const waitingForParent = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const item of items) {
|
|
31
|
+
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
32
|
+
if (nodes.has(id)) {
|
|
33
|
+
throw new Error(`Duplicate identifier "${id}".`);
|
|
34
|
+
}
|
|
35
|
+
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
36
|
+
nodes.set(id, node);
|
|
37
|
+
const parentId = typeof parentIdAccessor === "function" ? parentIdAccessor(item) : item[parentIdAccessor];
|
|
38
|
+
const parentNode = nodes.get(parentId);
|
|
39
|
+
if (parentNode) {
|
|
40
|
+
parentNode[nodeChildrenKey] ||= [];
|
|
41
|
+
parentNode[nodeChildrenKey].push(node);
|
|
42
|
+
if (nodeParentKey !== false) {
|
|
43
|
+
node[nodeParentKey] = parentNode;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
const siblings = waitingForParent.get(parentId);
|
|
47
|
+
if (siblings) {
|
|
48
|
+
siblings.push(node);
|
|
49
|
+
} else {
|
|
50
|
+
waitingForParent.set(parentId, [node]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const children = waitingForParent.get(id);
|
|
54
|
+
if (children) {
|
|
55
|
+
node[nodeChildrenKey] = children;
|
|
56
|
+
if (nodeParentKey !== false) {
|
|
57
|
+
for (const child of children) {
|
|
58
|
+
child[nodeParentKey] = node;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
waitingForParent.delete(id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const [parentId, nodes2] of waitingForParent.entries()) {
|
|
65
|
+
if (validateReferences && parentId != null) {
|
|
66
|
+
throw new Error(`Referential integrity violation: parentId "${parentId}" does not match any item in the input.`);
|
|
67
|
+
}
|
|
68
|
+
for (const node of nodes2) {
|
|
69
|
+
roots.push(node);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else if (childIdsAccessor) {
|
|
73
|
+
const waitingChildren = /* @__PURE__ */ new Map();
|
|
74
|
+
const rootCandidates = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
77
|
+
if (nodes.has(id)) {
|
|
78
|
+
throw new Error(`Duplicate identifier "${id}".`);
|
|
79
|
+
}
|
|
80
|
+
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
81
|
+
nodes.set(id, node);
|
|
82
|
+
const childIds = typeof childIdsAccessor === "function" ? childIdsAccessor(item) : item[childIdsAccessor];
|
|
83
|
+
if (childIds != null) {
|
|
84
|
+
if (typeof childIds[Symbol.iterator] !== "function") {
|
|
85
|
+
throw new Error(`Item "${id}" has invalid children: expected an iterable value.`);
|
|
86
|
+
}
|
|
87
|
+
node[nodeChildrenKey] = [];
|
|
88
|
+
for (const childId of childIds) {
|
|
89
|
+
const childNode = nodes.get(childId);
|
|
90
|
+
if (childNode) {
|
|
91
|
+
node[nodeChildrenKey].push(childNode);
|
|
92
|
+
if (nodeParentKey !== false) {
|
|
93
|
+
if (childNode[nodeParentKey] && childNode[nodeParentKey] !== node) {
|
|
94
|
+
throw new Error(`Node "${childId}" already has a different parent, refusing to overwrite. Set "nodeParentKey" to false to supress this error.`);
|
|
95
|
+
}
|
|
96
|
+
childNode[nodeParentKey] = node;
|
|
97
|
+
}
|
|
98
|
+
rootCandidates.delete(childId);
|
|
99
|
+
} else {
|
|
100
|
+
waitingChildren.set(childId, {
|
|
101
|
+
parentNode: node,
|
|
102
|
+
childIndex: node[nodeChildrenKey].length
|
|
103
|
+
});
|
|
104
|
+
node[nodeChildrenKey].push(null);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (node[nodeChildrenKey].length === 0) {
|
|
108
|
+
delete node[nodeChildrenKey];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const parentDescriptor = waitingChildren.get(id);
|
|
112
|
+
if (parentDescriptor) {
|
|
113
|
+
const { parentNode, childIndex } = parentDescriptor;
|
|
114
|
+
parentNode[nodeChildrenKey][childIndex] = node;
|
|
115
|
+
if (nodeParentKey !== false) {
|
|
116
|
+
if (node[nodeParentKey] && node[nodeParentKey] !== parentNode) {
|
|
117
|
+
throw new Error(`Node "${id}" already has a different parent, refusing to overwrite. Set "nodeParentKey" to false to supress this error.`);
|
|
118
|
+
}
|
|
119
|
+
node[nodeParentKey] = parentNode;
|
|
120
|
+
}
|
|
121
|
+
waitingChildren.delete(id);
|
|
122
|
+
} else {
|
|
123
|
+
rootCandidates.set(id, node);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (waitingChildren.size > 0) {
|
|
127
|
+
if (validateReferences) {
|
|
128
|
+
const childId = waitingChildren.keys().next().value;
|
|
129
|
+
throw new Error(`Referential integrity violation: child reference "${childId}" does not match any item in the input.`);
|
|
130
|
+
}
|
|
131
|
+
const pending = Array.from(waitingChildren.values());
|
|
132
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
133
|
+
const { parentNode, childIndex } = pending[i];
|
|
134
|
+
parentNode[nodeChildrenKey].splice(childIndex, 1);
|
|
135
|
+
if (parentNode[nodeChildrenKey].length === 0) {
|
|
136
|
+
delete parentNode[nodeChildrenKey];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const node of rootCandidates.values()) {
|
|
141
|
+
roots.push(node);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (validateTree || withDepth) {
|
|
145
|
+
if (roots.length === 0 && nodes.size > 0) {
|
|
146
|
+
throw new Error("Tree validation failed: detected a cycle.");
|
|
147
|
+
}
|
|
148
|
+
const stack = [...roots].map((node) => ({ node, depth: 0 }));
|
|
149
|
+
const visited = /* @__PURE__ */ new Set();
|
|
150
|
+
let processedCount = 0;
|
|
151
|
+
const MAX_NODES = nodes.size;
|
|
152
|
+
while (stack.length > 0 && processedCount++ <= MAX_NODES) {
|
|
153
|
+
const { node, depth } = stack.pop();
|
|
154
|
+
if (visited.has(node)) {
|
|
155
|
+
throw new Error("Tree validation failed: a node reachable via multiple paths.");
|
|
156
|
+
}
|
|
157
|
+
visited.add(node);
|
|
158
|
+
if (withDepth) {
|
|
159
|
+
node.depth = depth;
|
|
160
|
+
}
|
|
161
|
+
if (node[nodeChildrenKey]) {
|
|
162
|
+
for (const child of node[nodeChildrenKey]) {
|
|
163
|
+
stack.push({ node: child, depth: depth + 1 });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (nodes.size !== visited.size) {
|
|
168
|
+
throw new Error("Tree validation failed: detected a cycle.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { roots, nodes };
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
buildTree as default
|
|
175
|
+
};
|
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-tree-builder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.0",
|
|
4
4
|
"description": "Efficiently construct highly customizable bi-directional tree structures from iterable data.",
|
|
5
|
-
"
|
|
6
|
-
"module": "./index.
|
|
5
|
+
"types": "./index.d.mts",
|
|
6
|
+
"module": "./index.mjs",
|
|
7
7
|
"main": "./index.cjs",
|
|
8
|
-
"types": "./index.d.ts",
|
|
9
8
|
"exports": {
|
|
10
9
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
10
|
+
"types": "./index.d.mts",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"require": "./index.cjs"
|
|
14
13
|
}
|
|
15
14
|
},
|
|
16
15
|
"scripts": {
|
|
17
16
|
"prepack": "npm run build && npm run test",
|
|
18
17
|
"postversion": "git push && git push --tags",
|
|
19
|
-
"clean": "node scripts/clean.
|
|
20
|
-
"build": "node scripts/build.
|
|
21
|
-
"watch": "node scripts/watch.
|
|
18
|
+
"clean": "node scripts/clean.mjs",
|
|
19
|
+
"build": "node scripts/build.mjs",
|
|
20
|
+
"watch": "node scripts/watch.mjs",
|
|
22
21
|
"test": "mocha",
|
|
23
22
|
"coverage": "c8 -r text -r text-summary -r lcov --include \"*.js\" npm test"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
25
|
"c8": "^10.1.3",
|
|
27
26
|
"chokidar": "^4.0.3",
|
|
27
|
+
"esbuild": "^0.25.4",
|
|
28
28
|
"mocha": "^11.1.0",
|
|
29
29
|
"typescript": "^5.3.3"
|
|
30
30
|
},
|
package/index.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
type TreeNode<TInputData, TDataKey extends string | number | symbol | false, TParentKey extends string | number | symbol | false, TChildrenKey extends string | number | symbol> = (TDataKey extends false ? (TParentKey extends false ? Omit<TInputData, TChildrenKey> & {
|
|
2
|
-
[k in TChildrenKey]?: TreeNode<TInputData, TDataKey, TParentKey, TChildrenKey>[];
|
|
3
|
-
} : Omit<TInputData, Exclude<TParentKey, false> | TChildrenKey> & {
|
|
4
|
-
[k in Exclude<TParentKey, false>]?: TreeNode<TInputData, TDataKey, TParentKey, TChildrenKey>;
|
|
5
|
-
} & {
|
|
6
|
-
[k in TChildrenKey]?: TreeNode<TInputData, TDataKey, TParentKey, TChildrenKey>[];
|
|
7
|
-
}) : (TParentKey extends false ? {
|
|
8
|
-
[k in Exclude<TDataKey, false>]: TInputData;
|
|
9
|
-
} & {
|
|
10
|
-
[k in TChildrenKey]?: TreeNode<TInputData, TDataKey, TParentKey, TChildrenKey>[];
|
|
11
|
-
} : {
|
|
12
|
-
[k in Exclude<TDataKey, false>]: TInputData;
|
|
13
|
-
} & {
|
|
14
|
-
[k in Exclude<TParentKey, false>]?: TreeNode<TInputData, TDataKey, TParentKey, TChildrenKey>;
|
|
15
|
-
} & {
|
|
16
|
-
[k in TChildrenKey]?: TreeNode<TInputData, TDataKey, TParentKey, TChildrenKey>[];
|
|
17
|
-
}));
|
|
18
|
-
type KeyReturnType<T, P extends keyof T | ((item: T) => any)> = P extends ((item: T) => infer R) ? R : P extends keyof T ? T[P] : never;
|
|
19
|
-
declare function buildTree<TInputData extends (TNodeDataKey extends false ? object : unknown), TKey extends keyof TInputData | ((item: TInputData) => any), TMappedData extends (TNodeDataKey extends false ? object : unknown) = TInputData, TNodeDataKey extends string | number | symbol | false = 'data', TNodeParentKey extends string | number | symbol | false = 'parent', TNodeChildrenKey extends string | number | symbol = 'children'>(items: Iterable<TInputData>, options?: {
|
|
20
|
-
mode?: 'parent' | 'children';
|
|
21
|
-
key?: TKey;
|
|
22
|
-
parentKey?: keyof TInputData | ((item: TInputData) => any);
|
|
23
|
-
childrenKey?: keyof TInputData | ((item: TInputData) => any);
|
|
24
|
-
nodeDataKey?: TNodeDataKey;
|
|
25
|
-
nodeParentKey?: TNodeParentKey;
|
|
26
|
-
nodeChildrenKey?: TNodeChildrenKey;
|
|
27
|
-
mapNodeData?: {
|
|
28
|
-
(item: TInputData): TMappedData;
|
|
29
|
-
};
|
|
30
|
-
validRootKeys?: Iterable<unknown>;
|
|
31
|
-
validRootParentKeys?: Iterable<unknown>;
|
|
32
|
-
validateTree?: boolean;
|
|
33
|
-
}): {
|
|
34
|
-
roots: TreeNode<TMappedData extends undefined ? TInputData : TMappedData, TNodeDataKey, TNodeParentKey, TNodeChildrenKey>[];
|
|
35
|
-
nodes: Map<KeyReturnType<TInputData, TKey>, TreeNode<TMappedData extends undefined ? TInputData : TMappedData, TNodeDataKey, TNodeParentKey, TNodeChildrenKey>>;
|
|
36
|
-
};
|
|
37
|
-
export default buildTree;
|
package/index.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
function buildTree(items, options = {}) {
|
|
2
|
-
const { mode = 'parent', key = 'id', parentKey = 'parent', childrenKey = 'children', nodeDataKey = 'data', nodeParentKey = 'parent', nodeChildrenKey = 'children', mapNodeData, validRootKeys, validRootParentKeys, validateTree = false, } = options;
|
|
3
|
-
const roots = [];
|
|
4
|
-
const nodes = new Map();
|
|
5
|
-
const danglingNodes = new Map();
|
|
6
|
-
if (mode === 'parent') {
|
|
7
|
-
for (const item of items) {
|
|
8
|
-
const keyOfNode = typeof key === 'function' ? key(item) : item[key];
|
|
9
|
-
const keyOfParentNode = typeof parentKey === 'function' ? parentKey(item) : item[parentKey];
|
|
10
|
-
if (nodes.has(keyOfNode)) {
|
|
11
|
-
throw new Error(`Duplicate identifier detected for "${keyOfNode}"`);
|
|
12
|
-
}
|
|
13
|
-
// Current node can be new or already created by a child item as its parent
|
|
14
|
-
let node = danglingNodes.get(keyOfNode);
|
|
15
|
-
if (node) {
|
|
16
|
-
danglingNodes.delete(keyOfNode);
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
node = {};
|
|
20
|
-
}
|
|
21
|
-
nodes.set(keyOfNode, node);
|
|
22
|
-
// Set the data of the node
|
|
23
|
-
const nodeData = typeof mapNodeData === 'function' ? mapNodeData(item) : item;
|
|
24
|
-
if (nodeDataKey !== false) {
|
|
25
|
-
node[nodeDataKey] = nodeData;
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
Object.assign(node, nodeData);
|
|
29
|
-
}
|
|
30
|
-
// Link this node to its parent
|
|
31
|
-
let parentNode = nodes.get(keyOfParentNode) ?? danglingNodes.get(keyOfParentNode);
|
|
32
|
-
if (!parentNode) {
|
|
33
|
-
// No parent node exists yet, create as dangling node
|
|
34
|
-
parentNode = {};
|
|
35
|
-
// Track as dangling node, we dont know yet if it really exists
|
|
36
|
-
danglingNodes.set(keyOfParentNode, parentNode);
|
|
37
|
-
}
|
|
38
|
-
// When no children added yet
|
|
39
|
-
if (!parentNode[nodeChildrenKey]) {
|
|
40
|
-
parentNode[nodeChildrenKey] = [];
|
|
41
|
-
}
|
|
42
|
-
// Add as child
|
|
43
|
-
parentNode[nodeChildrenKey].push(node);
|
|
44
|
-
// Set the parent on this node
|
|
45
|
-
if (nodeParentKey !== false) {
|
|
46
|
-
node[nodeParentKey] = parentNode;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Children of dangling nodes will become the root nodes
|
|
50
|
-
if (validRootParentKeys) {
|
|
51
|
-
const validParentKeys = new Set(validRootParentKeys);
|
|
52
|
-
for (const [parentKey, node] of danglingNodes.entries()) {
|
|
53
|
-
if (!validParentKeys.has(parentKey)) {
|
|
54
|
-
throw new Error(`Invalid parent key "${parentKey}" found for a root node.`);
|
|
55
|
-
}
|
|
56
|
-
for (const root of node[nodeChildrenKey]) {
|
|
57
|
-
// Root nodes does not have a parent, unlink the dangling node
|
|
58
|
-
if (nodeParentKey !== false) {
|
|
59
|
-
delete root[nodeParentKey];
|
|
60
|
-
}
|
|
61
|
-
roots.push(root);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
for (const node of danglingNodes.values()) {
|
|
67
|
-
for (const root of node[nodeChildrenKey]) {
|
|
68
|
-
// Root nodes does not have a parent, unlink the dangling node
|
|
69
|
-
if (nodeParentKey !== false) {
|
|
70
|
-
delete root[nodeParentKey];
|
|
71
|
-
}
|
|
72
|
-
roots.push(root);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// TODO: this could be optimized
|
|
77
|
-
if (validRootKeys) {
|
|
78
|
-
const rootsSet = new Set(roots);
|
|
79
|
-
const validKeys = new Set(validRootKeys);
|
|
80
|
-
for (const [key, node] of nodes) {
|
|
81
|
-
if (rootsSet.has(node) && !validKeys.has(key)) {
|
|
82
|
-
throw new Error(`A root node has an invalid key "${key}"`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
if (validRootParentKeys) {
|
|
89
|
-
throw new Error(`Option "validRootParentKeys" cannot be used when mode is set to "children".`);
|
|
90
|
-
}
|
|
91
|
-
const knownNodes = new Set();
|
|
92
|
-
const incompleteNodes = new Set();
|
|
93
|
-
for (const item of items) {
|
|
94
|
-
const keyOfNode = typeof key === 'function' ? key(item) : item[key];
|
|
95
|
-
const keyOfChildNodes = typeof childrenKey === 'function' ? childrenKey(item) : item[childrenKey];
|
|
96
|
-
if (knownNodes.has(keyOfNode)) {
|
|
97
|
-
throw new Error(`Duplicate identifier detected for "${keyOfNode}"`);
|
|
98
|
-
}
|
|
99
|
-
knownNodes.add(keyOfNode);
|
|
100
|
-
incompleteNodes.delete(keyOfNode);
|
|
101
|
-
let node = nodes.get(keyOfNode);
|
|
102
|
-
if (!node) {
|
|
103
|
-
node = {};
|
|
104
|
-
danglingNodes.set(keyOfNode, node);
|
|
105
|
-
}
|
|
106
|
-
// Set the data of the node
|
|
107
|
-
const nodeData = typeof mapNodeData === 'function' ? mapNodeData(item) : item;
|
|
108
|
-
if (nodeDataKey !== false) {
|
|
109
|
-
node[nodeDataKey] = nodeData;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
Object.assign(node, nodeData);
|
|
113
|
-
}
|
|
114
|
-
// Link children to this node
|
|
115
|
-
if (keyOfChildNodes) {
|
|
116
|
-
node[nodeChildrenKey] = [];
|
|
117
|
-
for (const keyOfChildNode of keyOfChildNodes) {
|
|
118
|
-
let childNode = danglingNodes.get(keyOfChildNode);
|
|
119
|
-
if (childNode) {
|
|
120
|
-
nodes.set(keyOfChildNode, childNode);
|
|
121
|
-
danglingNodes.delete(keyOfChildNode);
|
|
122
|
-
// Set the parent on child node
|
|
123
|
-
if (nodeParentKey !== false) {
|
|
124
|
-
childNode[nodeParentKey] = node;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
else if (nodes.has(keyOfChildNode)) {
|
|
128
|
-
throw new Error(`Duplicate parent detected for "${keyOfChildNode}"`);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
// We create a new temporary node
|
|
132
|
-
childNode = {};
|
|
133
|
-
// Set the parent on child node
|
|
134
|
-
if (nodeParentKey !== false) {
|
|
135
|
-
childNode[nodeParentKey] = node;
|
|
136
|
-
}
|
|
137
|
-
nodes.set(keyOfChildNode, childNode);
|
|
138
|
-
incompleteNodes.add(keyOfChildNode);
|
|
139
|
-
}
|
|
140
|
-
node[nodeChildrenKey].push(childNode);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (incompleteNodes.size > 0) {
|
|
145
|
-
throw new Error(`Some nodes miss their referenced children (count: ${incompleteNodes.size}, references: ${JSON.stringify([...incompleteNodes])}).`);
|
|
146
|
-
}
|
|
147
|
-
if (validRootKeys) {
|
|
148
|
-
const validKeys = new Set(validRootKeys);
|
|
149
|
-
for (const [key, node] of danglingNodes.entries()) {
|
|
150
|
-
if (!validKeys.has(key)) {
|
|
151
|
-
throw new Error(`A root node has an invalid key "${key}"`);
|
|
152
|
-
}
|
|
153
|
-
roots.push(node);
|
|
154
|
-
nodes.set(key, node);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
for (const [key, node] of danglingNodes.entries()) {
|
|
159
|
-
roots.push(node);
|
|
160
|
-
nodes.set(key, node);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (validateTree) {
|
|
165
|
-
if (nodes.size > 0 && danglingNodes.size === 0) {
|
|
166
|
-
throw new Error('Tree validation error: Stucture is a cyclic graph.');
|
|
167
|
-
}
|
|
168
|
-
// Count nodes, if count === nodes.size then no cycles.
|
|
169
|
-
const gray = [...roots];
|
|
170
|
-
let count = 0;
|
|
171
|
-
let node;
|
|
172
|
-
while ((node = gray.pop()) && count <= nodes.size) {
|
|
173
|
-
++count;
|
|
174
|
-
if (node[nodeChildrenKey]) {
|
|
175
|
-
for (const child of node[nodeChildrenKey]) {
|
|
176
|
-
gray.push(child);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (count !== nodes.size) {
|
|
181
|
-
throw new Error('Tree validation error: Stucture is a cyclic graph.');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return { roots, nodes };
|
|
185
|
-
}
|
|
186
|
-
export default buildTree;
|