fast-tree-builder 0.0.1
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/LICENSE +19 -0
- package/README.md +200 -0
- package/cjs/index.d.ts +36 -0
- package/cjs/index.js +85 -0
- package/cjs/package.json +1 -0
- package/esm/index.d.ts +36 -0
- package/esm/index.js +83 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright 2023 László BULIK
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
4
|
+
copy of this software and associated documentation files (the “Software”),
|
|
5
|
+
to deal in the Software without restriction, including without limitation
|
|
6
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
|
8
|
+
Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
|
11
|
+
all copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# fast-tree-builder
|
|
2
|
+
|
|
3
|
+
[](https://github.com/lionel87/fast-tree-builder/actions/workflows/coveralls.yaml)
|
|
4
|
+
[](https://coveralls.io/github/lionel87/fast-tree-builder?branch=master)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
`fast-tree-builder` is an npm package that allows you to efficiently build trees from iterable data structures. With its optimized algorithm, strong TypeScript typings, and customizable node structure, it provides a reliable solution for organizing and manipulating hierarchical data.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
Efficient Tree Building: The package utilizes an optimized algorithm to construct trees efficiently, ensuring quick performance even with large datasets.
|
|
14
|
+
|
|
15
|
+
- **Bi-Directional Tree Traversal**: Traverse the built tree in both directions, enabling easy navigation between parent and child nodes.
|
|
16
|
+
|
|
17
|
+
- **Strong TypeScript Typings**: Enjoy the benefits of type safety with comprehensive TypeScript typings. The package provides detailed type definitions to enhance your development experience.
|
|
18
|
+
|
|
19
|
+
- **Fully Customizable Node Structure**: Tailor the structure of the nodes in the built tree 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.
|
|
20
|
+
|
|
21
|
+
- **Works on Any Iterable Data Structure**: Whether you have an array, set, or any other iterable data structure, `fast-tree-builder` can process it seamlessly, making it compatible with a wide range of use cases.
|
|
22
|
+
|
|
23
|
+
- **No Sorting Required**: The package does not require your input data to be sorted, saving you preprocessing time and effort.
|
|
24
|
+
|
|
25
|
+
- **Flexible Key and Parent Key Types**: You can use any JavaScript type as keys. Items are matched by comparing `key === parentKey`.
|
|
26
|
+
|
|
27
|
+
- **Map of Nodes**: Retrieve a `Map` object containing the nodes of the built tree, enabling easy entry and access on any point of the tree.
|
|
28
|
+
|
|
29
|
+
- **Support for Multiple Root Nodes**: The package supports building trees with multiple root nodes.
|
|
30
|
+
|
|
31
|
+
- **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.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
To install `fast-tree-builder`, use npm:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install fast-tree-builder
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
yarn add fast-tree-builder
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Here are some examples showcasing the usage of `fast-tree-builder` and their expected outputs:
|
|
52
|
+
|
|
53
|
+
### Example 1: Basic Tree Building
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import buildTree from 'fast-tree-builder';
|
|
57
|
+
|
|
58
|
+
const items = [
|
|
59
|
+
{ id: 1, parent: null, name: 'Root 1' },
|
|
60
|
+
{ id: 2, parent: null, name: 'Root 2' },
|
|
61
|
+
{ id: 3, parent: 1, name: 'Child 1.1' },
|
|
62
|
+
{ id: 4, parent: 1, name: 'Child 1.2' },
|
|
63
|
+
{ id: 5, parent: 2, name: 'Child 2.1' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const { roots, nodes } = buildTree(items, {
|
|
67
|
+
key: 'id',
|
|
68
|
+
parentKey: 'parent',
|
|
69
|
+
nodeDataKey: 'data',
|
|
70
|
+
nodeParentKey: 'parent',
|
|
71
|
+
nodeChildrenKey: 'children',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(roots[0].data.name);
|
|
75
|
+
// Expected output: Root 1
|
|
76
|
+
|
|
77
|
+
console.log(roots[0].children[1].data.name);
|
|
78
|
+
// Expected output: Child 1.2
|
|
79
|
+
|
|
80
|
+
console.log(roots[0].children[1].parent.data.name);
|
|
81
|
+
// Expected output: Root 1
|
|
82
|
+
|
|
83
|
+
console.log(roots);
|
|
84
|
+
// Expected output: [
|
|
85
|
+
// { data: { id: 1, parent: null, name: 'Root 1' }, children: [
|
|
86
|
+
// { data: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
|
|
87
|
+
// { data: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } }
|
|
88
|
+
// ] },
|
|
89
|
+
// { data: { id: 2, parent: null, name: 'Root 2' }, children: [
|
|
90
|
+
// { data: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
|
|
91
|
+
// ] }
|
|
92
|
+
// ]
|
|
93
|
+
|
|
94
|
+
console.log(nodes);
|
|
95
|
+
// Expected output: Map {
|
|
96
|
+
// 1 => { 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: { ... } }
|
|
99
|
+
// ] },
|
|
100
|
+
// 2 => { data: { id: 2, parent: null, name: 'Root 2' }, children: [
|
|
101
|
+
// { data: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
|
|
102
|
+
// ] },
|
|
103
|
+
// 3 => { data: { id: 3, parent: 1, name: 'Child 1.1' }, parent: { ... } },
|
|
104
|
+
// 4 => { data: { id: 4, parent: 1, name: 'Child 1.2' }, parent: { ... } },
|
|
105
|
+
// 5 => { data: { id: 5, parent: 2, name: 'Child 2.1' }, parent: { ... } }
|
|
106
|
+
// }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
### Example 2: Customized Node Structure
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import buildTree from 'fast-tree-builder';
|
|
114
|
+
|
|
115
|
+
const items = [
|
|
116
|
+
{ key: { n: 1 }, parentKey: null, name: 'Root 1' },
|
|
117
|
+
{ key: { n: 2 }, parentKey: null, name: 'Root 2' },
|
|
118
|
+
{ key: { n: 3 }, parentKey: { n: 1 }, name: 'Child 1.1' },
|
|
119
|
+
{ key: { n: 4 }, parentKey: { n: 1 }, name: 'Child 1.2' },
|
|
120
|
+
{ key: { n: 5 }, parentKey: { n: 2 }, name: 'Child 2.1' },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const { roots, nodes } = buildTree(items, {
|
|
124
|
+
key(item) { return item.key?.n; },
|
|
125
|
+
parentKey(item) { return item.parentKey?.n; },
|
|
126
|
+
nodeDataKey: false, // don't store data separately on the node
|
|
127
|
+
nodeParentKey: 'up',
|
|
128
|
+
nodeChildrenKey: 'down',
|
|
129
|
+
mapNodeData(item) { return { title: item.name }; },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(roots[0].title);
|
|
133
|
+
// Expected output: Root 1
|
|
134
|
+
|
|
135
|
+
console.log(roots[0].down[1].title);
|
|
136
|
+
// Expected output: Child 1.2
|
|
137
|
+
|
|
138
|
+
console.log(roots[0].down[1].up.title);
|
|
139
|
+
// Expected output: Root 1
|
|
140
|
+
|
|
141
|
+
console.log(roots);
|
|
142
|
+
// Expected output: [
|
|
143
|
+
// { title: 'Root 1', down: [
|
|
144
|
+
// { title: 'Child 1.1', up: { ... } },
|
|
145
|
+
// { title: 'Child 1.2', up: { ... } }
|
|
146
|
+
// ] },
|
|
147
|
+
// { title: 'Root 2', down: [
|
|
148
|
+
// { title: 'Child 2.1', up: { ... } }
|
|
149
|
+
// ] }
|
|
150
|
+
// ]
|
|
151
|
+
|
|
152
|
+
console.log(nodes);
|
|
153
|
+
// Expected output: Map {
|
|
154
|
+
// 1 => { title: 'Root 1', down: [
|
|
155
|
+
// { title: 'Child 1.1', up: { ... } },
|
|
156
|
+
// { title: 'Child 1.2', up: { ... } }
|
|
157
|
+
// ] },
|
|
158
|
+
// 2 => { title: 'Root 2', down: [
|
|
159
|
+
// { title: 'Child 2.1', up: { ... } }
|
|
160
|
+
// ] },
|
|
161
|
+
// 3 => { title: 'Child 1.1', up: { ... } },
|
|
162
|
+
// 4 => { title: 'Child 1.2', up: { ... } },
|
|
163
|
+
// 5 => { title: 'Child 2.1', up: { ... } }
|
|
164
|
+
// }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
## Documentation
|
|
169
|
+
|
|
170
|
+
### `buildTree(items: Iterable<T>, options: BuildTreeOptions): TreeResult<T>`
|
|
171
|
+
|
|
172
|
+
Builds a tree from the given iterable `items` using the specified `options`.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
|
|
176
|
+
- `items`: An iterable data structure containing the items to build the tree from.
|
|
177
|
+
- `options`: An object specifying the build options. It has the following properties:
|
|
178
|
+
- `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'`.
|
|
179
|
+
- `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'`.
|
|
180
|
+
- `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'`.
|
|
181
|
+
- `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'`.
|
|
182
|
+
- `nodeChildrenKey`: (Optional) The key used to store the children nodes in each node. It can be a string, number, symbol, or false if the children nodes should not be included. Defaults to `'children'`.
|
|
183
|
+
- `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`.
|
|
184
|
+
- `validateParentKeys`: (Optional) An iterable containing keys that should be validated as existing parent keys. If provided, any item with a parent key not present in this iterable will cause an error to be thrown. Defaults to `undefined`.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
|
|
188
|
+
An object with the following properties:
|
|
189
|
+
|
|
190
|
+
- `roots`: An array of the root nodes of the built tree.
|
|
191
|
+
- `nodes`: A `Map` object containing all nodes of the built tree, with keys corresponding to their identifiers.
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
## Contributions
|
|
195
|
+
|
|
196
|
+
Contributions to `fast-tree-builder` are welcome! If you have any bug reports, feature requests, or improvements, please open an issue on the [GitHub repository](https://github.com/lionel87/fast-tree-builder).
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
`fast-tree-builder` is licensed under the [MIT License](https://github.com/lionel87/fast-tree-builder/blob/main/LICENSE).
|
package/cjs/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type TreeNode<T, D extends string | number | symbol | false, P extends string | number | symbol | false, C extends string | number | symbol> = (D extends false ? (P extends false ? T & {
|
|
2
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
3
|
+
} : T & {
|
|
4
|
+
[k in Exclude<P, false>]?: TreeNode<T, D, P, C>;
|
|
5
|
+
} & {
|
|
6
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
7
|
+
}) : (P extends false ? {
|
|
8
|
+
[k in Exclude<D, false>]: T;
|
|
9
|
+
} & {
|
|
10
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
11
|
+
} : {
|
|
12
|
+
[k in Exclude<D, false>]: T;
|
|
13
|
+
} & {
|
|
14
|
+
[k in Exclude<P, false>]?: TreeNode<T, D, P, C>;
|
|
15
|
+
} & {
|
|
16
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
17
|
+
}));
|
|
18
|
+
declare function buildTree<T extends (D extends false ? object : unknown), M extends (D extends false ? object : unknown) = T, K extends unknown = unknown, D extends string | number | symbol | false = 'data', P extends string | number | symbol | false = 'parent', C extends string | number | symbol = 'children'>(items: Iterable<T>, options: {
|
|
19
|
+
key?: string | number | symbol | {
|
|
20
|
+
(item: T): K;
|
|
21
|
+
};
|
|
22
|
+
parentKey?: string | number | symbol | {
|
|
23
|
+
(item: T): K;
|
|
24
|
+
};
|
|
25
|
+
nodeDataKey?: D;
|
|
26
|
+
nodeParentKey?: P;
|
|
27
|
+
nodeChildrenKey?: C;
|
|
28
|
+
mapNodeData?: {
|
|
29
|
+
(item: T): M;
|
|
30
|
+
};
|
|
31
|
+
validateParentKeys?: Iterable<unknown>;
|
|
32
|
+
}): {
|
|
33
|
+
roots: TreeNode<M extends undefined ? T : M, D, P, C>[];
|
|
34
|
+
nodes: Map<K, TreeNode<M extends undefined ? T : M, D, P, C>>;
|
|
35
|
+
};
|
|
36
|
+
export default buildTree;
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
function buildTree(items, options) {
|
|
4
|
+
var _a;
|
|
5
|
+
const { key = 'id', parentKey = 'parent', nodeDataKey = 'data', nodeParentKey = 'parent', nodeChildrenKey = 'children', mapNodeData, validateParentKeys, } = options;
|
|
6
|
+
const nodes = new Map();
|
|
7
|
+
const danglingNodes = new Map();
|
|
8
|
+
for (const item of items) {
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
const keyOfNode = typeof key === 'function' ? key(item) : item[key];
|
|
11
|
+
if (nodes.has(keyOfNode)) {
|
|
12
|
+
throw new Error(`Duplicate identifier detected for "${keyOfNode}"`);
|
|
13
|
+
}
|
|
14
|
+
// Current node can be new or already created by a child item as its parent
|
|
15
|
+
let node = danglingNodes.get(keyOfNode);
|
|
16
|
+
if (node) {
|
|
17
|
+
danglingNodes.delete(keyOfNode);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
node = {};
|
|
21
|
+
}
|
|
22
|
+
nodes.set(keyOfNode, node);
|
|
23
|
+
// Set the data of the node
|
|
24
|
+
const nodeData = typeof mapNodeData === 'function' ? mapNodeData(item) : item;
|
|
25
|
+
if (nodeDataKey !== false) {
|
|
26
|
+
node[nodeDataKey] = nodeData;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
Object.assign(node, nodeData);
|
|
30
|
+
}
|
|
31
|
+
// Link this node to its parent
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
const keyOfParentNode = typeof parentKey === 'function' ? parentKey(item) : item[parentKey];
|
|
34
|
+
let parent = (_a = nodes.get(keyOfParentNode)) !== null && _a !== void 0 ? _a : danglingNodes.get(keyOfParentNode);
|
|
35
|
+
if (!parent) {
|
|
36
|
+
// No parent node exists yet, create as dangling node
|
|
37
|
+
parent = {};
|
|
38
|
+
// Track as dangling node, we dont know yet if it really exists
|
|
39
|
+
danglingNodes.set(keyOfParentNode, parent);
|
|
40
|
+
}
|
|
41
|
+
// When no children added yet
|
|
42
|
+
if (!parent[nodeChildrenKey]) {
|
|
43
|
+
parent[nodeChildrenKey] = [];
|
|
44
|
+
}
|
|
45
|
+
// Add as child
|
|
46
|
+
parent[nodeChildrenKey].push(node);
|
|
47
|
+
// Set the parent on this node
|
|
48
|
+
if (nodeParentKey !== false) {
|
|
49
|
+
node[nodeParentKey] = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (danglingNodes.size === 0) {
|
|
53
|
+
throw new Error(`Could not find root nodes, circular references found.`);
|
|
54
|
+
}
|
|
55
|
+
// Children of dangling nodes will become the root nodes
|
|
56
|
+
const roots = [];
|
|
57
|
+
if (validateParentKeys) {
|
|
58
|
+
const validParentKeys = new Set(validateParentKeys);
|
|
59
|
+
for (const [key, node] of danglingNodes.entries()) {
|
|
60
|
+
if (!validParentKeys.has(key)) {
|
|
61
|
+
throw new Error(`Invalid parent key "${key}" from items "${node[nodeChildrenKey].join('", "')}".`);
|
|
62
|
+
}
|
|
63
|
+
for (const root of node[nodeChildrenKey]) {
|
|
64
|
+
// Root nodes does not have a parent, unlink the dangling node
|
|
65
|
+
if (nodeParentKey !== false) {
|
|
66
|
+
delete root[nodeParentKey];
|
|
67
|
+
}
|
|
68
|
+
roots.push(root);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
for (const node of danglingNodes.values()) {
|
|
74
|
+
for (const root of node[nodeChildrenKey]) {
|
|
75
|
+
// Root nodes does not have a parent, unlink the dangling node
|
|
76
|
+
if (nodeParentKey !== false) {
|
|
77
|
+
delete root[nodeParentKey];
|
|
78
|
+
}
|
|
79
|
+
roots.push(root);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { roots, nodes };
|
|
84
|
+
}
|
|
85
|
+
exports.default = buildTree;
|
package/cjs/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "type": "commonjs" }
|
package/esm/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type TreeNode<T, D extends string | number | symbol | false, P extends string | number | symbol | false, C extends string | number | symbol> = (D extends false ? (P extends false ? T & {
|
|
2
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
3
|
+
} : T & {
|
|
4
|
+
[k in Exclude<P, false>]?: TreeNode<T, D, P, C>;
|
|
5
|
+
} & {
|
|
6
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
7
|
+
}) : (P extends false ? {
|
|
8
|
+
[k in Exclude<D, false>]: T;
|
|
9
|
+
} & {
|
|
10
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
11
|
+
} : {
|
|
12
|
+
[k in Exclude<D, false>]: T;
|
|
13
|
+
} & {
|
|
14
|
+
[k in Exclude<P, false>]?: TreeNode<T, D, P, C>;
|
|
15
|
+
} & {
|
|
16
|
+
[k in C]?: TreeNode<T, D, P, C>[];
|
|
17
|
+
}));
|
|
18
|
+
declare function buildTree<T extends (D extends false ? object : unknown), M extends (D extends false ? object : unknown) = T, K extends unknown = unknown, D extends string | number | symbol | false = 'data', P extends string | number | symbol | false = 'parent', C extends string | number | symbol = 'children'>(items: Iterable<T>, options: {
|
|
19
|
+
key?: string | number | symbol | {
|
|
20
|
+
(item: T): K;
|
|
21
|
+
};
|
|
22
|
+
parentKey?: string | number | symbol | {
|
|
23
|
+
(item: T): K;
|
|
24
|
+
};
|
|
25
|
+
nodeDataKey?: D;
|
|
26
|
+
nodeParentKey?: P;
|
|
27
|
+
nodeChildrenKey?: C;
|
|
28
|
+
mapNodeData?: {
|
|
29
|
+
(item: T): M;
|
|
30
|
+
};
|
|
31
|
+
validateParentKeys?: Iterable<unknown>;
|
|
32
|
+
}): {
|
|
33
|
+
roots: TreeNode<M extends undefined ? T : M, D, P, C>[];
|
|
34
|
+
nodes: Map<K, TreeNode<M extends undefined ? T : M, D, P, C>>;
|
|
35
|
+
};
|
|
36
|
+
export default buildTree;
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function buildTree(items, options) {
|
|
2
|
+
var _a;
|
|
3
|
+
const { key = 'id', parentKey = 'parent', nodeDataKey = 'data', nodeParentKey = 'parent', nodeChildrenKey = 'children', mapNodeData, validateParentKeys, } = options;
|
|
4
|
+
const nodes = new Map();
|
|
5
|
+
const danglingNodes = new Map();
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
const keyOfNode = typeof key === 'function' ? key(item) : item[key];
|
|
9
|
+
if (nodes.has(keyOfNode)) {
|
|
10
|
+
throw new Error(`Duplicate identifier detected for "${keyOfNode}"`);
|
|
11
|
+
}
|
|
12
|
+
// Current node can be new or already created by a child item as its parent
|
|
13
|
+
let node = danglingNodes.get(keyOfNode);
|
|
14
|
+
if (node) {
|
|
15
|
+
danglingNodes.delete(keyOfNode);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
node = {};
|
|
19
|
+
}
|
|
20
|
+
nodes.set(keyOfNode, node);
|
|
21
|
+
// Set the data of the node
|
|
22
|
+
const nodeData = typeof mapNodeData === 'function' ? mapNodeData(item) : item;
|
|
23
|
+
if (nodeDataKey !== false) {
|
|
24
|
+
node[nodeDataKey] = nodeData;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
Object.assign(node, nodeData);
|
|
28
|
+
}
|
|
29
|
+
// Link this node to its parent
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
const keyOfParentNode = typeof parentKey === 'function' ? parentKey(item) : item[parentKey];
|
|
32
|
+
let parent = (_a = nodes.get(keyOfParentNode)) !== null && _a !== void 0 ? _a : danglingNodes.get(keyOfParentNode);
|
|
33
|
+
if (!parent) {
|
|
34
|
+
// No parent node exists yet, create as dangling node
|
|
35
|
+
parent = {};
|
|
36
|
+
// Track as dangling node, we dont know yet if it really exists
|
|
37
|
+
danglingNodes.set(keyOfParentNode, parent);
|
|
38
|
+
}
|
|
39
|
+
// When no children added yet
|
|
40
|
+
if (!parent[nodeChildrenKey]) {
|
|
41
|
+
parent[nodeChildrenKey] = [];
|
|
42
|
+
}
|
|
43
|
+
// Add as child
|
|
44
|
+
parent[nodeChildrenKey].push(node);
|
|
45
|
+
// Set the parent on this node
|
|
46
|
+
if (nodeParentKey !== false) {
|
|
47
|
+
node[nodeParentKey] = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (danglingNodes.size === 0) {
|
|
51
|
+
throw new Error(`Could not find root nodes, circular references found.`);
|
|
52
|
+
}
|
|
53
|
+
// Children of dangling nodes will become the root nodes
|
|
54
|
+
const roots = [];
|
|
55
|
+
if (validateParentKeys) {
|
|
56
|
+
const validParentKeys = new Set(validateParentKeys);
|
|
57
|
+
for (const [key, node] of danglingNodes.entries()) {
|
|
58
|
+
if (!validParentKeys.has(key)) {
|
|
59
|
+
throw new Error(`Invalid parent key "${key}" from items "${node[nodeChildrenKey].join('", "')}".`);
|
|
60
|
+
}
|
|
61
|
+
for (const root of node[nodeChildrenKey]) {
|
|
62
|
+
// Root nodes does not have a parent, unlink the dangling node
|
|
63
|
+
if (nodeParentKey !== false) {
|
|
64
|
+
delete root[nodeParentKey];
|
|
65
|
+
}
|
|
66
|
+
roots.push(root);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (const node of danglingNodes.values()) {
|
|
72
|
+
for (const root of node[nodeChildrenKey]) {
|
|
73
|
+
// Root nodes does not have a parent, unlink the dangling node
|
|
74
|
+
if (nodeParentKey !== false) {
|
|
75
|
+
delete root[nodeParentKey];
|
|
76
|
+
}
|
|
77
|
+
roots.push(root);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { roots, nodes };
|
|
82
|
+
}
|
|
83
|
+
export default buildTree;
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fast-tree-builder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Efficiently construct highly customizable bi-directional tree structures from iterable data.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "cjs/index.js",
|
|
7
|
+
"module": "esm/index.js",
|
|
8
|
+
"types": "esm/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"require": "./cjs/index.js",
|
|
12
|
+
"default": "./esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"prepack": "npm run build",
|
|
17
|
+
"clean": "rimraf esm cjs",
|
|
18
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
19
|
+
"build:esm": "tsc",
|
|
20
|
+
"watch:esm": "tsc --watch",
|
|
21
|
+
"build:cjs": "tsc --outDir cjs --module commonjs && echo { \"type\": \"commonjs\" }>cjs/package.json",
|
|
22
|
+
"watch:cjs": "npm run build:cjs && tsc --outDir cjs --module commonjs --watch",
|
|
23
|
+
"test": "node test.js"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/lionel87/fast-tree-builder.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"tree builder",
|
|
31
|
+
"tree structure",
|
|
32
|
+
"iterable data",
|
|
33
|
+
"typescript",
|
|
34
|
+
"type safe",
|
|
35
|
+
"bi-directional traversal",
|
|
36
|
+
"list to tree",
|
|
37
|
+
"array to tree",
|
|
38
|
+
"unflatten tree",
|
|
39
|
+
"algorithm",
|
|
40
|
+
"navigation",
|
|
41
|
+
"breadcrumbs",
|
|
42
|
+
"descendants",
|
|
43
|
+
"ancestors",
|
|
44
|
+
"tree",
|
|
45
|
+
"builder",
|
|
46
|
+
"children",
|
|
47
|
+
"child",
|
|
48
|
+
"nested",
|
|
49
|
+
"list",
|
|
50
|
+
"validation"
|
|
51
|
+
],
|
|
52
|
+
"author": "László BULIK",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/lionel87/fast-tree-builder/issues"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/lionel87/fast-tree-builder#readme",
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"rimraf": "^5.0.1",
|
|
60
|
+
"typescript": "^5.1.3"
|
|
61
|
+
}
|
|
62
|
+
}
|