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 CHANGED
@@ -6,7 +6,8 @@
6
6
  [![Coverage Status](https://coveralls.io/repos/github/lionel87/fast-tree-builder/badge.svg?branch=master)](https://coveralls.io/github/lionel87/fast-tree-builder?branch=master)
7
7
  ![Maintenance](https://img.shields.io/maintenance/yes/2025)
8
8
 
9
- `fast-tree-builder` is an npm package that allows you to build trees quickly from iterable data structures. With its optimized algorithm, strong TypeScript typings, and customizable node structure it provides a convenient solution for working with hierarchical data.
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
- - **Efficient Tree Building**: Using an optimized algorithm to construct trees in `O(n)` time.
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
- - **Robust TypeScript Type Definitions**: Type safety through extensive TypeScript type definitions which helps code reliability and developer workflow.
34
+ ## Installation
24
35
 
25
- - **Fully Customizable Node Structure**: The structure of the nodes in the built tree is customizable to meet your specific requirements. You have the freedom to define `data`, `parent`, and `children` key names according to your application's needs. To avoid circular references, parent links can be turned off which helps generating JSON data.
36
+ ```sh
37
+ npm install fast-tree-builder
38
+ ```
26
39
 
27
- - **Works on Iterables**: Designed to handle arrays, sets, and other iterable data structures out of the box ensuring broad applicability.
40
+ or
28
41
 
29
- - **No Sorting Required**: The algorithm does not require your input data to be sorted (eg. parent must come before children), saving you preprocessing time and effort.
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
- - **Multiple Root Nodes**: Can construct multiple distinct trees. Handy when the intent is to handle the set as one tree, but a 'virtual' root item is not present among the items to couple them together. The 'roots' becomes your virtual root in this case.
47
+ ## Documentation
34
48
 
35
- - **Map of Nodes**: Beside the root nodes you can retrieve a `Map` object containing the nodes of the built tree, enabling easy entry on any point of the tree.
49
+ ### `buildTree(items: Iterable<T>, options: Options): TreeResult`
36
50
 
37
- - **Support for Parent Key Validation**: Enables you to validate parent keys while building the tree. When a node missing its parent, an error will be thrown.
51
+ Builds a tree structure from an iterable list of items.
38
52
 
39
- - **Support for Tree Validation**: Ensures the recieved data structure is an acyclic graph.
53
+ #### Parameters
40
54
 
41
- ## Installation
55
+ * `items`: Any iterable of input items.
56
+ * `options`: Configuration object:
42
57
 
43
- To install `fast-tree-builder`, use npm:
58
+ ##### Required
44
59
 
45
- ```sh
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
- or
62
+ ##### One of:
50
63
 
51
- ```sh
52
- yarn add fast-tree-builder
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
- key: 'id',
78
- parentKey: 'parent',
115
+ id: 'id',
116
+ parentId: 'parent',
79
117
  // the built node:
80
- nodeDataKey: 'data',
118
+ nodeValueKey: 'value',
81
119
  nodeParentKey: 'parent',
82
120
  nodeChildrenKey: 'children',
83
121
  });
84
122
 
85
- console.log(roots[0].data.name);
123
+ console.log(roots[0].value.name);
86
124
  // Expected output: Root 1
87
125
 
88
- console.log(roots[0].children[1].data.name);
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.data.name);
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
- // { data: { id: 1, parent: null, name: 'Root 1' }, children: [
97
- // { data: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
98
- // { data: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } }
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
- // { data: { id: 2, parent: null, name: 'Root 2' }, children: [
101
- // { data: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
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 => { data: { id: 1, parent: null, name: 'Root 1' }, children: [
108
- // { data: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
109
- // { data: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } }
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 => { data: { id: 2, parent: null, name: 'Root 2' }, children: [
112
- // { data: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
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 => { data: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
115
- // 4 => { data: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } },
116
- // 5 => { data: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
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
- mode: 'children'
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
- key(item) { return item.key?.n; },
156
- parentKey(item) { return item.parentKey?.n; },
157
- nodeDataKey: false, // merge item data into node
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
- key(item) { return item.substring(2, 4); },
214
- parentKey(item) { return item.substring(0, 2); },
215
- mapNodeData(item) { return { name: item.substring(4) }; },
216
- nodeDataKey: false, // merge item data into node
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.data = item; // Or Object.assign(node, item);
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(exports, "__esModule", { value: true });
3
- function buildTree(items, options = {}) {
4
- const { mode = 'parent', key = 'id', parentKey = 'parent', childrenKey = 'children', nodeDataKey = 'data', nodeParentKey = 'parent', nodeChildrenKey = 'children', mapNodeData, validRootKeys, validRootParentKeys, validateTree = false, } = options;
5
- const roots = [];
6
- const nodes = new Map();
7
- const danglingNodes = new Map();
8
- if (mode === 'parent') {
9
- for (const item of items) {
10
- const keyOfNode = typeof key === 'function' ? key(item) : item[key];
11
- const keyOfParentNode = typeof parentKey === 'function' ? parentKey(item) : item[parentKey];
12
- if (nodes.has(keyOfNode)) {
13
- throw new Error(`Duplicate identifier detected for "${keyOfNode}"`);
14
- }
15
- // Current node can be new or already created by a child item as its parent
16
- let node = danglingNodes.get(keyOfNode);
17
- if (node) {
18
- danglingNodes.delete(keyOfNode);
19
- }
20
- else {
21
- node = {};
22
- }
23
- nodes.set(keyOfNode, node);
24
- // Set the data of the node
25
- const nodeData = typeof mapNodeData === 'function' ? mapNodeData(item) : item;
26
- if (nodeDataKey !== false) {
27
- node[nodeDataKey] = nodeData;
28
- }
29
- else {
30
- Object.assign(node, nodeData);
31
- }
32
- // Link this node to its parent
33
- let parentNode = nodes.get(keyOfParentNode) ?? danglingNodes.get(keyOfParentNode);
34
- if (!parentNode) {
35
- // No parent node exists yet, create as dangling node
36
- parentNode = {};
37
- // Track as dangling node, we dont know yet if it really exists
38
- danglingNodes.set(keyOfParentNode, parentNode);
39
- }
40
- // When no children added yet
41
- if (!parentNode[nodeChildrenKey]) {
42
- parentNode[nodeChildrenKey] = [];
43
- }
44
- // Add as child
45
- parentNode[nodeChildrenKey].push(node);
46
- // Set the parent on this node
47
- if (nodeParentKey !== false) {
48
- node[nodeParentKey] = parentNode;
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
- // Children of dangling nodes will become the root nodes
52
- if (validRootParentKeys) {
53
- const validParentKeys = new Set(validRootParentKeys);
54
- for (const [parentKey, node] of danglingNodes.entries()) {
55
- if (!validParentKeys.has(parentKey)) {
56
- throw new Error(`Invalid parent key "${parentKey}" found for a root node.`);
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
- else {
68
- for (const node of danglingNodes.values()) {
69
- for (const root of node[nodeChildrenKey]) {
70
- // Root nodes does not have a parent, unlink the dangling node
71
- if (nodeParentKey !== false) {
72
- delete root[nodeParentKey];
73
- }
74
- roots.push(root);
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
- else {
90
- if (validRootParentKeys) {
91
- throw new Error(`Option "validRootParentKeys" cannot be used when mode is set to "children".`);
92
- }
93
- const knownNodes = new Set();
94
- const incompleteNodes = new Set();
95
- for (const item of items) {
96
- const keyOfNode = typeof key === 'function' ? key(item) : item[key];
97
- const keyOfChildNodes = typeof childrenKey === 'function' ? childrenKey(item) : item[childrenKey];
98
- if (knownNodes.has(keyOfNode)) {
99
- throw new Error(`Duplicate identifier detected for "${keyOfNode}"`);
100
- }
101
- knownNodes.add(keyOfNode);
102
- incompleteNodes.delete(keyOfNode);
103
- let node = nodes.get(keyOfNode);
104
- if (!node) {
105
- node = {};
106
- danglingNodes.set(keyOfNode, node);
107
- }
108
- // Set the data of the node
109
- const nodeData = typeof mapNodeData === 'function' ? mapNodeData(item) : item;
110
- if (nodeDataKey !== false) {
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
- if (incompleteNodes.size > 0) {
147
- throw new Error(`Some nodes miss their referenced children (count: ${incompleteNodes.size}, references: ${JSON.stringify([...incompleteNodes])}).`);
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 (validRootKeys) {
150
- const validKeys = new Set(validRootKeys);
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
- else {
160
- for (const [key, node] of danglingNodes.entries()) {
161
- roots.push(node);
162
- nodes.set(key, node);
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 (validateTree) {
167
- if (nodes.size > 0 && danglingNodes.size === 0) {
168
- throw new Error('Tree validation error: Stucture is a cyclic graph.');
169
- }
170
- // Count nodes, if count === nodes.size then no cycles.
171
- const gray = [...roots];
172
- let count = 0;
173
- let node;
174
- while ((node = gray.pop()) && count <= nodes.size) {
175
- ++count;
176
- if (node[nodeChildrenKey]) {
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
- if (count !== nodes.size) {
183
- throw new Error('Tree validation error: Stucture is a cyclic graph.');
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
- return { roots, nodes };
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": "1.0.2",
3
+ "version": "2.0.0-alpha.0",
4
4
  "description": "Efficiently construct highly customizable bi-directional tree structures from iterable data.",
5
- "type": "module",
6
- "module": "./index.js",
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
- "import": "./index.js",
12
- "require": "./index.cjs",
13
- "types": "./index.d.ts"
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.js",
20
- "build": "node scripts/build.js",
21
- "watch": "node scripts/watch.js",
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;