tree-processor 0.8.0 → 0.8.2

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.en.md ADDED
@@ -0,0 +1,1026 @@
1
+ # tree-processor
2
+
3
+ <div align="right">
4
+
5
+ [English](https://github.com/knott11/tree-processor/blob/main/README.en.md) | [中文](https://github.com/knott11/tree-processor/blob/main/README.md)
6
+
7
+ </div>
8
+
9
+ <div align="center">
10
+
11
+ ![npm version](https://img.shields.io/npm/v/tree-processor?style=flat-square)
12
+ ![npm downloads](https://img.shields.io/npm/dm/tree-processor?style=flat-square)
13
+ ![bundle size](https://img.shields.io/badge/bundle-6.6KB-blue?style=flat-square)
14
+ ![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)
15
+ ![coverage](https://img.shields.io/badge/coverage-99%25-brightgreen?style=flat-square)
16
+
17
+ A lightweight tree-structured data processing utility library written in TypeScript, supporting tree-shaking, with each format bundle size approximately **6-7 KB** (ESM: 6.39 KB, CJS: 6.64 KB, UMD: 6.68 KB).
18
+
19
+
20
+ </div>
21
+
22
+ ## 📋 Table of Contents
23
+
24
+ - [Features](#-features)
25
+ - [Use Cases](#-use-cases)
26
+ - [Installation](#-installation)
27
+ - [Quick Start](#-quick-start)
28
+ - [API Documentation](#-api-documentation)
29
+ - [Traversal Methods](#traversal-methods)
30
+ - [Search Methods](#search-methods)
31
+ - [Access Methods](#access-methods)
32
+ - [Modification Methods](#modification-methods)
33
+ - [Query Methods](#query-methods)
34
+ - [Validation Methods](#validation-methods)
35
+ - [Custom Field Names](#custom-field-names)
36
+ - [Testing](#testing)
37
+ - [Development](#development)
38
+
39
+ ## ✨ Features
40
+
41
+ - **Lightweight** - Each format bundle size is only 6-7 KB (ESM: 6.39 KB, CJS: 6.64 KB, UMD: 6.68 KB), minimal impact on project size
42
+ - **Tree-shaking Support** - Supports on-demand imports, only bundles the code you actually use, further reducing bundle size
43
+ - **Full TypeScript Support** - Provides complete type definitions and IntelliSense, improving development experience
44
+ - **Flexible Custom Field Names** - Supports custom children and id field names, adapting to various data structures
45
+ - **Zero Dependencies** - No external dependencies, ready to use out of the box, no need to worry about dependency conflicts
46
+ - **Comprehensive Test Coverage** - Contains 290 test cases with 99%+ test coverage (99.67% statement coverage, 99.32% branch coverage, 100% function coverage), covering basic functionality, edge cases, error handling, and complex scenarios
47
+ - **Rich API** - Provides 30+ methods, including array-like APIs (map, filter, find, some, every, includes, at, indexOf, etc.), and tree-specific operations (get parent/child nodes, depth calculation, data validation, etc.), covering complete scenarios for traversal, search, modification, and validation
48
+
49
+ **Supported methods:** mapTree, forEachTree, filterTree, findTree, pushTree, unshiftTree, popTree, shiftTree, someTree, everyTree, includesTree, atTree, indexOfTree, atIndexOfTree, dedupTree, removeTree, getParentTree, getChildrenTree, getSiblingsTree, getNodeDepthMap, getNodeDepth, isLeafNode, isRootNode, isEmptyTreeData, isEmptySingleTreeData, isTreeData, isSingleTreeData, isValidTreeNode, isTreeNodeWithCircularCheck, isSafeTreeDepth. The last parameter of each method can customize the property names for children and id.
50
+
51
+ ### 💡 Use Cases
52
+
53
+ - **Navigation Systems** - Multi-level menus, route configuration expansion, collapse, search, and filtering
54
+ - **File Systems** - File directory traversal, search, move, and delete
55
+ - **Permission Systems** - Organizational structure, role permission tree structure management and validation
56
+ - **Framework Development** - Component trees, route trees, and other tree structure construction and management
57
+ - **Data Management** - Category management, comment systems, tree selectors, and other data operations
58
+
59
+ ## 📦 Installation
60
+
61
+ ```bash
62
+ npm install tree-processor
63
+ # or
64
+ yarn add tree-processor
65
+ # or
66
+ pnpm add tree-processor
67
+ ```
68
+
69
+ ## 🚀 Quick Start
70
+
71
+ ```javascript
72
+ import { mapTree, findTree, filterTree } from 'tree-processor'
73
+
74
+ const treeData = [
75
+ {
76
+ id: 1,
77
+ name: 'node1',
78
+ children: [
79
+ { id: 2, name: 'node2' },
80
+ { id: 3, name: 'node3' },
81
+ ],
82
+ },
83
+ ]
84
+
85
+ // Get all node names
86
+ const names = mapTree(treeData, (node) => node.name)
87
+ console.log(names) // ['node1', 'node2', 'node3']
88
+
89
+ // Find a node
90
+ const node = findTree(treeData, (n) => n.id === 2)
91
+ console.log(node) // { id: 2, name: 'node2' }
92
+
93
+ // Filter nodes
94
+ const filtered = filterTree(treeData, (n) => n.id > 1)
95
+ console.log(filtered) // [{ id: 2, name: 'node2' }, { id: 3, name: 'node3' }]
96
+ ```
97
+
98
+ ## 📖 API Documentation
99
+
100
+ ### Import Methods
101
+
102
+ #### Default Import (Recommended for scenarios requiring multiple methods)
103
+
104
+ ```javascript
105
+ // ES Module
106
+ import t from 'tree-processor'
107
+
108
+ // CommonJS
109
+ const t = require('tree-processor')
110
+ ```
111
+
112
+ #### Named Import (Recommended for scenarios requiring only a few methods, supports tree-shaking)
113
+
114
+ ```javascript
115
+ // ES Module - Named import of individual methods
116
+ import { mapTree, filterTree, findTree } from 'tree-processor'
117
+
118
+ // ES Module - Import types
119
+ import type { TreeNode, TreeData, FieldNames } from 'tree-processor'
120
+
121
+ // CommonJS - Named import
122
+ const { mapTree, filterTree } = require('tree-processor')
123
+ ```
124
+
125
+ ### Sample Data
126
+
127
+ The following sample data will be used for demonstrations of all subsequent methods:
128
+
129
+ ```javascript
130
+ const treeData = [
131
+ {
132
+ id: 1,
133
+ name: 'node1',
134
+ children: [
135
+ {
136
+ id: 2,
137
+ name: 'node2',
138
+ children: [
139
+ { id: 4, name: 'node4' },
140
+ { id: 5, name: 'node5' },
141
+ ],
142
+ },
143
+ {
144
+ id: 3,
145
+ name: 'node3',
146
+ children: [{ id: 6, name: 'node6' }],
147
+ },
148
+ ],
149
+ },
150
+ ];
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Traversal Methods
156
+
157
+ ### mapTree
158
+
159
+ Traverse tree-structured data and execute a callback function for each node, returning a mapped array.
160
+
161
+ ```javascript
162
+ // Get all node names
163
+ const nodeNames = t.mapTree(treeData, (node) => node.name)
164
+ console.log(nodeNames) // ['node1', 'node2', 'node4', 'node5', 'node3', 'node6']
165
+
166
+ // Get all node IDs
167
+ const nodeIds = t.mapTree(treeData, (node) => node.id)
168
+ console.log(nodeIds) // [1, 2, 4, 5, 3, 6]
169
+
170
+ // Modify node data
171
+ const modifiedNodes = t.mapTree(treeData, (node) => ({
172
+ ...node,
173
+ label: node.name
174
+ }))
175
+ console.log(modifiedNodes) // Returns a new array containing the label field
176
+ ```
177
+
178
+ ### forEachTree
179
+
180
+ Traverse tree-structured data and execute a callback function for each node. Unlike mapTree, this doesn't return a value and has better performance, suitable for scenarios where you only need to traverse without returning results.
181
+
182
+ ```javascript
183
+ // Traverse all nodes and print
184
+ t.forEachTree(treeData, (node) => {
185
+ console.log(node)
186
+ })
187
+
188
+ // Modify node properties
189
+ t.forEachTree(treeData, (node) => {
190
+ node.visited = true
191
+ node.timestamp = Date.now()
192
+ })
193
+
194
+ // Count nodes
195
+ let nodeCount = 0
196
+ t.forEachTree(treeData, () => {
197
+ nodeCount++
198
+ })
199
+ console.log(nodeCount) // Total number of nodes
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Search Methods
205
+
206
+ ### filterTree
207
+
208
+ Filter tree-structured data and return nodes that meet the conditions.
209
+
210
+ ```javascript
211
+ // Filter nodes whose names contain 'node'
212
+ const filteredNodes = t.filterTree(treeData, (node) => {
213
+ return node.name.includes('node')
214
+ })
215
+ console.log(filteredNodes) // Returns an array of nodes that meet the conditions
216
+
217
+ // Filter nodes with ID greater than 2
218
+ const nodesWithLargeId = t.filterTree(treeData, (node) => node.id > 2)
219
+ console.log(nodesWithLargeId) // Returns an array of nodes with ID greater than 2
220
+
221
+ // Filter nodes without children (leaf nodes)
222
+ const leafNodes = t.filterTree(treeData, (node) => {
223
+ return !node.children || node.children.length === 0
224
+ })
225
+ console.log(leafNodes) // Returns all leaf nodes
226
+ ```
227
+
228
+ ### findTree
229
+
230
+ Find the first node in tree-structured data that meets the condition. Returns null if not found.
231
+
232
+ ```javascript
233
+ // Find node with ID 2
234
+ const foundNode = t.findTree(treeData, (node) => node.id === 2)
235
+ console.log(foundNode) // Returns the found node object, or null if not found
236
+
237
+ // Find node with name 'node3'
238
+ const node3 = t.findTree(treeData, (node) => node.name === 'node3')
239
+ console.log(node3) // { id: 3, name: 'node3', children: [...] }
240
+
241
+ // Find a non-existent node
242
+ const nodeNotFound = t.findTree(treeData, (node) => node.id === 999)
243
+ console.log(nodeNotFound) // null
244
+ ```
245
+
246
+ ### includesTree
247
+
248
+ Check if tree-structured data contains a node with the specified ID.
249
+
250
+ ```javascript
251
+ const nodeId = 2
252
+ const hasNode = t.includesTree(treeData, nodeId)
253
+
254
+ console.log(hasNode) // true means it contains the node, false means it doesn't
255
+ ```
256
+
257
+ ### someTree
258
+
259
+ Check if there exists a node in tree-structured data that meets the condition. Returns true if at least one node meets the condition.
260
+
261
+ ```javascript
262
+ // Check if there exists a node with name 'node2'
263
+ const hasNode2 = t.someTree(treeData, node => node.name === 'node2')
264
+ console.log(hasNode2) // true
265
+
266
+ // Check if there exists a node with ID greater than 10
267
+ const hasLargeId = t.someTree(treeData, node => node.id > 10)
268
+ console.log(hasLargeId) // false
269
+ ```
270
+
271
+ ### everyTree
272
+
273
+ Check if all nodes in tree-structured data meet the condition. Returns true only if all nodes meet the condition.
274
+
275
+ ```javascript
276
+ // Check if all node IDs are greater than 0
277
+ const allIdsPositive = t.everyTree(treeData, node => node.id > 0)
278
+ console.log(allIdsPositive) // true
279
+
280
+ // Check if all nodes have a name property
281
+ const allHaveName = t.everyTree(treeData, node => node.name)
282
+ console.log(allHaveName) // Returns true or false based on actual data
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Access Methods
288
+
289
+ ### atTree
290
+
291
+ Get a node by parent node ID and child node index. Supports negative indices, same as the array at method. Returns null if not found.
292
+
293
+ ```javascript
294
+ // Get the first child node (index 0) of the node with ID 1
295
+ const firstChildNode = t.atTree(treeData, 1, 0)
296
+ console.log(firstChildNode) // Returns the first child node
297
+
298
+ // Get the last child node (negative index)
299
+ const lastChildNode = t.atTree(treeData, 1, -1)
300
+ console.log(lastChildNode) // Returns the last child node
301
+
302
+ // Index out of range returns null
303
+ const nodeNotFound = t.atTree(treeData, 1, 10)
304
+ console.log(nodeNotFound) // null
305
+ ```
306
+
307
+ ### indexOfTree
308
+
309
+ Returns an array representing the index path from the root node to the node with the targetId. Returns null if not found. The return value can be passed to the second parameter of atIndexOfTree for value retrieval.
310
+
311
+ ```javascript
312
+ // Get the index path of the node with ID 4
313
+ const nodePath = t.indexOfTree(treeData, 4)
314
+ console.log(nodePath) // [0, 0, 0] means root node -> first child -> first child
315
+
316
+ // Node not found returns null
317
+ const pathNotFound = t.indexOfTree(treeData, 999)
318
+ console.log(pathNotFound) // null
319
+
320
+ // Use with atIndexOfTree
321
+ const indexPath = t.indexOfTree(treeData, 4)
322
+ const nodeByPath = t.atIndexOfTree(treeData, indexPath)
323
+ console.log(nodeByPath) // Gets the node with ID 4
324
+ ```
325
+
326
+ ### atIndexOfTree
327
+
328
+ Get a node by index path. Returns null if the path is invalid or out of range.
329
+
330
+ ```javascript
331
+ // Get node by index path
332
+ const nodeByIndexPath = t.atIndexOfTree(treeData, [0, 1, 0])
333
+ console.log(nodeByIndexPath) // Returns the node object at the corresponding path
334
+
335
+ // Use with indexOfTree
336
+ const targetPath = t.indexOfTree(treeData, 4)
337
+ const targetNode = t.atIndexOfTree(treeData, targetPath)
338
+ console.log(targetNode) // Gets the node with ID 4
339
+
340
+ // Invalid path returns null
341
+ const invalidPath = t.atIndexOfTree(treeData, [999])
342
+ console.log(invalidPath) // null
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Modification Methods
348
+
349
+ ### pushTree
350
+
351
+ Add a child node to the end under the specified node. Returns true if successful, false if the target node is not found.
352
+
353
+ ```javascript
354
+ // Add a new child node under the node with ID 1
355
+ const addSuccess = t.pushTree(treeData, 1, { id: 7, name: 'node7' })
356
+ console.log(addSuccess) // true
357
+ console.log(treeData) // New node has been added to the end of the children array
358
+
359
+ // Try to add under a non-existent node
360
+ const addFailed = t.pushTree(treeData, 999, { id: 8, name: 'node8' })
361
+ console.log(addFailed) // false, target node not found
362
+ ```
363
+
364
+ ### unshiftTree
365
+
366
+ Add a child node to the beginning under the specified node. Returns true if successful, false if the target node is not found.
367
+
368
+ ```javascript
369
+ // Add a new child node to the beginning under the node with ID 1
370
+ const unshiftSuccess = t.unshiftTree(treeData, 1, { id: 7, name: 'node7' })
371
+ console.log(unshiftSuccess) // true
372
+ console.log(treeData) // New node has been added to the beginning of the children array
373
+ ```
374
+
375
+ ### popTree
376
+
377
+ Remove the last child node under the specified node. Returns the removed node, or false if the node doesn't exist or has no children.
378
+
379
+ ```javascript
380
+ // Remove the last child node under the node with ID 1
381
+ const removedNode = t.popTree(treeData, 1)
382
+ console.log(removedNode) // Returns the removed node object, or false
383
+
384
+ // Try to remove from a non-existent node
385
+ const popFailed = t.popTree(treeData, 999)
386
+ console.log(popFailed) // false
387
+ ```
388
+
389
+ ### shiftTree
390
+
391
+ Remove the first child node under the specified node. Returns the removed node, or false if the node doesn't exist or has no children.
392
+
393
+ ```javascript
394
+ // Remove the first child node under the node with ID 1
395
+ const shiftedNode = t.shiftTree(treeData, 1)
396
+ console.log(shiftedNode) // Returns the removed node object, or false
397
+ ```
398
+
399
+ ### removeTree
400
+
401
+ Remove the node with the specified ID from tree-structured data, including root nodes and child nodes.
402
+
403
+ ```javascript
404
+ const nodeIdToRemove = 2
405
+ const removeSuccess = t.removeTree(treeData, nodeIdToRemove)
406
+
407
+ console.log(removeSuccess) // true means successful removal, false means node not found
408
+ console.log(treeData) // Tree structure after removal
409
+ ```
410
+
411
+ ### dedupTree
412
+
413
+ Tree-structured object array deduplication method that removes duplicate nodes based on the specified key. Keeps the first occurrence of the node.
414
+
415
+ ```javascript
416
+ // Deduplicate based on id field
417
+ const uniqueTreeData = t.dedupTree(treeData, 'id')
418
+ console.log(uniqueTreeData) // Returns deduplicated tree-structured data
419
+
420
+ // Deduplicate based on name field
421
+ const uniqueByNameTree = t.dedupTree(treeData, 'name')
422
+ console.log(uniqueByNameTree) // Returns data deduplicated by name
423
+ ```
424
+
425
+ ---
426
+
427
+ ## Query Methods
428
+
429
+ ### getParentTree
430
+
431
+ Get the parent node of the specified node. Returns null if the node is a root node or not found.
432
+
433
+ ```javascript
434
+ // Get the parent node of the node with ID 2
435
+ const parentNode = t.getParentTree(treeData, 2)
436
+ console.log(parentNode) // Returns the parent node object { id: 1, name: 'node1', ... }
437
+
438
+ // Root nodes have no parent, returns null
439
+ const rootParentNode = t.getParentTree(treeData, 1)
440
+ console.log(rootParentNode) // null
441
+
442
+ // Node not found returns null
443
+ const parentNotFound = t.getParentTree(treeData, 999)
444
+ console.log(parentNotFound) // null
445
+ ```
446
+
447
+ ### getChildrenTree
448
+
449
+ Get all direct child nodes of the specified node. Returns an empty array if the node is not found or has no children.
450
+
451
+ ```javascript
452
+ // Get all child nodes of the node with ID 1
453
+ const children = t.getChildrenTree(treeData, 1)
454
+ console.log(children) // Returns child node array [{ id: 2, ... }, { id: 3, ... }]
455
+
456
+ // Node has no children, returns empty array
457
+ const emptyChildren = t.getChildrenTree(treeData, 4)
458
+ console.log(emptyChildren) // []
459
+
460
+ // Node not found returns empty array
461
+ const notFound = t.getChildrenTree(treeData, 999)
462
+ console.log(notFound) // []
463
+
464
+ // Support custom field names
465
+ const customTree = [
466
+ {
467
+ nodeId: 1,
468
+ name: 'root',
469
+ subNodes: [
470
+ { nodeId: 2, name: 'child1' },
471
+ { nodeId: 3, name: 'child2' },
472
+ ],
473
+ },
474
+ ];
475
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
476
+ const customChildren = t.getChildrenTree(customTree, 1, fieldNames)
477
+ console.log(customChildren) // Returns child node array
478
+ ```
479
+
480
+ ### getSiblingsTree
481
+
482
+ Get all sibling nodes of the specified node (including itself). Returns an empty array if the node is not found. Sibling nodes of root nodes are other root nodes.
483
+
484
+ ```javascript
485
+ // Get all sibling nodes (including itself) of the node with ID 2
486
+ const siblings = t.getSiblingsTree(treeData, 2)
487
+ console.log(siblings) // Returns sibling node array [{ id: 2, ... }, { id: 3, ... }]
488
+
489
+ // Sibling nodes of root nodes are other root nodes
490
+ const multiRoot = [
491
+ { id: 1, children: [{ id: 2 }] },
492
+ { id: 3, children: [{ id: 4 }] },
493
+ ];
494
+ const rootSiblings = t.getSiblingsTree(multiRoot, 1)
495
+ console.log(rootSiblings) // Returns all root nodes [{ id: 1, ... }, { id: 3, ... }]
496
+
497
+ // Node not found returns empty array
498
+ const notFound = t.getSiblingsTree(treeData, 999)
499
+ console.log(notFound) // []
500
+
501
+ // Support custom field names
502
+ const customTree = [
503
+ {
504
+ nodeId: 1,
505
+ name: 'root',
506
+ subNodes: [
507
+ { nodeId: 2, name: 'child1' },
508
+ { nodeId: 3, name: 'child2' },
509
+ { nodeId: 4, name: 'child3' },
510
+ ],
511
+ },
512
+ ];
513
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
514
+ const customSiblings = t.getSiblingsTree(customTree, 2, fieldNames)
515
+ console.log(customSiblings) // Returns sibling node array (including itself)
516
+ ```
517
+
518
+ ### getNodeDepthMap
519
+
520
+ Returns a dictionary where keys represent node ids and values represent which layer the node is in the data. Depth starts from 1, root nodes have depth 1.
521
+
522
+ ```javascript
523
+ // Get depth map of all nodes
524
+ const nodeDepthMap = t.getNodeDepthMap(treeData)
525
+ console.log(nodeDepthMap) // { 1: 1, 2: 2, 3: 2, 4: 3, 5: 3, 6: 3 }
526
+
527
+ // Get depth of a specific node
528
+ const node2Depth = nodeDepthMap[2]
529
+ console.log(node2Depth) // 2
530
+
531
+ // Empty tree returns empty object
532
+ const emptyDepthMap = t.getNodeDepthMap([])
533
+ console.log(emptyDepthMap) // {}
534
+ ```
535
+
536
+ ### getNodeDepth
537
+
538
+ Get the depth of the specified node. Depth starts from 1, root nodes have depth 1.
539
+
540
+ ```javascript
541
+ // Get depth of root node
542
+ const rootDepth = t.getNodeDepth(treeData, 1)
543
+ console.log(rootDepth) // 1
544
+
545
+ // Get depth of child node
546
+ const childDepth = t.getNodeDepth(treeData, 2)
547
+ console.log(childDepth) // 2
548
+
549
+ // Get depth of deep node
550
+ const deepDepth = t.getNodeDepth(treeData, 4)
551
+ console.log(deepDepth) // 3
552
+
553
+ // Node not found returns null
554
+ const notFound = t.getNodeDepth(treeData, 999)
555
+ console.log(notFound) // null
556
+
557
+ // Support custom field names
558
+ const customTree = [
559
+ {
560
+ nodeId: 1,
561
+ name: 'root',
562
+ subNodes: [
563
+ { nodeId: 2, name: 'child' },
564
+ ],
565
+ },
566
+ ];
567
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
568
+ const depth = t.getNodeDepth(customTree, 2, fieldNames)
569
+ console.log(depth) // 2
570
+ ```
571
+
572
+ **Difference from getNodeDepthMap:**
573
+ - `getNodeDepthMap` - Batch get depth of all nodes (calculates all nodes at once)
574
+ - `getNodeDepth` - Only get depth of a single node (only calculates the target node, more efficient)
575
+
576
+ ---
577
+
578
+ ## Validation Methods
579
+
580
+ ### isLeafNode
581
+
582
+ Check if a node is a leaf node (has no children). Lightweight method that only checks the node itself, doesn't traverse the tree.
583
+
584
+ ```javascript
585
+ // Nodes without children field are leaf nodes
586
+ const leafNode1 = { id: 1, name: 'node1' };
587
+ console.log(t.isLeafNode(leafNode1)) // true
588
+
589
+ // Nodes with empty children array are leaf nodes
590
+ const leafNode2 = { id: 2, name: 'node2', children: [] };
591
+ console.log(t.isLeafNode(leafNode2)) // true
592
+
593
+ // Nodes with children are not leaf nodes
594
+ const parentNode = {
595
+ id: 3,
596
+ name: 'node3',
597
+ children: [{ id: 4, name: 'node4' }],
598
+ };
599
+ console.log(t.isLeafNode(parentNode)) // false
600
+
601
+ // Use in filterTree (filter out all leaf nodes)
602
+ const leafNodes = t.filterTree(treeData, (node) => t.isLeafNode(node))
603
+ console.log(leafNodes) // Returns all leaf nodes
604
+
605
+ // Use in forEachTree
606
+ t.forEachTree(treeData, (node) => {
607
+ if (t.isLeafNode(node)) {
608
+ console.log('Leaf node:', node.name)
609
+ }
610
+ })
611
+
612
+ // Support custom field names
613
+ const customNode = {
614
+ nodeId: 1,
615
+ name: 'node1',
616
+ subNodes: [],
617
+ };
618
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
619
+ console.log(t.isLeafNode(customNode, fieldNames)) // true
620
+ ```
621
+
622
+ **Difference from existing methods:**
623
+ - `isLeafNode` - Only checks a single node, lightweight (O(1)), suitable for use during traversal
624
+ - `getChildrenTree` - Gets child node array, requires passing tree and nodeId, needs to find the node (O(n))
625
+
626
+ ### isRootNode
627
+
628
+ Check if a node is a root node (has no parent). Root nodes are top-level nodes in the tree-structured data array.
629
+
630
+ ```javascript
631
+ // Check root node
632
+ const treeData = [
633
+ {
634
+ id: 1,
635
+ name: 'root1',
636
+ children: [{ id: 2, name: 'child1' }],
637
+ },
638
+ ];
639
+ console.log(t.isRootNode(treeData, 1)) // true
640
+ console.log(t.isRootNode(treeData, 2)) // false
641
+
642
+ // Multiple root nodes case
643
+ const multiRoot = [
644
+ { id: 1, name: 'root1' },
645
+ { id: 2, name: 'root2' },
646
+ { id: 3, name: 'root3' },
647
+ ];
648
+ console.log(t.isRootNode(multiRoot, 1)) // true
649
+ console.log(t.isRootNode(multiRoot, 2)) // true
650
+ console.log(t.isRootNode(multiRoot, 3)) // true
651
+
652
+ // Use during traversal
653
+ t.forEachTree(treeData, (node) => {
654
+ if (t.isRootNode(treeData, node.id)) {
655
+ console.log('Root node:', node.name)
656
+ }
657
+ })
658
+
659
+ // Support custom field names
660
+ const customTree = [
661
+ {
662
+ nodeId: 1,
663
+ name: 'root1',
664
+ subNodes: [{ nodeId: 2, name: 'child1' }],
665
+ },
666
+ ];
667
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
668
+ console.log(t.isRootNode(customTree, 1, fieldNames)) // true
669
+ console.log(t.isRootNode(customTree, 2, fieldNames)) // false
670
+
671
+ // Returns false when node doesn't exist
672
+ console.log(t.isRootNode(treeData, 999)) // false
673
+ ```
674
+
675
+ **Difference from existing methods:**
676
+ - `isRootNode` - Semantic method that directly returns boolean value
677
+ - `getParentTree` - Returns parent node object, need to check if it's null
678
+ - `getNodeDepth` - Returns depth, need to check if it equals 1
679
+
680
+ ### isEmptyTreeData
681
+
682
+ Check if tree-structured data (array) is empty. Empty arrays, null, and undefined are all considered empty. This function supports the fieldNames parameter to maintain API consistency, but the parameter has no effect (because it only checks if the array is empty, doesn't access children or id fields).
683
+
684
+ ```javascript
685
+ // Check if tree-structured data is empty
686
+ const isEmptyTree = t.isEmptyTreeData(treeData)
687
+ console.log(isEmptyTree) // false (has data)
688
+
689
+ // Empty array returns true
690
+ const isEmptyArray = t.isEmptyTreeData([])
691
+ console.log(isEmptyArray) // true
692
+
693
+ // null or undefined returns true
694
+ const isNullTree = t.isEmptyTreeData(null)
695
+ console.log(isNullTree) // true
696
+
697
+ // Support fieldNames parameter (maintains API consistency, but has no effect)
698
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
699
+ const isEmptyWithFieldNames = t.isEmptyTreeData(treeData, fieldNames)
700
+ console.log(isEmptyWithFieldNames) // false (same result as not passing fieldNames)
701
+ ```
702
+
703
+ ### isEmptySingleTreeData
704
+
705
+ Check if a single tree-structured data is empty. If the data is not valid single tree-structured data, has no children field, or children is an empty array, it is considered empty. If there are child nodes (children array is not empty), even if the child nodes themselves are empty, the tree is not empty.
706
+
707
+ ```javascript
708
+ // No children field, considered empty
709
+ const tree1 = { id: 1, name: 'node1' };
710
+ const isEmpty1 = t.isEmptySingleTreeData(tree1)
711
+ console.log(isEmpty1) // true
712
+
713
+ // children is empty array, considered empty
714
+ const tree2 = {
715
+ id: 1,
716
+ name: 'node1',
717
+ children: [],
718
+ };
719
+ const isEmpty2 = t.isEmptySingleTreeData(tree2)
720
+ console.log(isEmpty2) // true
721
+
722
+ // Has child nodes, not empty
723
+ const tree3 = {
724
+ id: 1,
725
+ name: 'node1',
726
+ children: [
727
+ { id: 2, name: 'node2' },
728
+ ],
729
+ };
730
+ const isEmpty3 = t.isEmptySingleTreeData(tree3)
731
+ console.log(isEmpty3) // false
732
+
733
+ // Has child nodes, even if child nodes themselves are empty, tree is not empty
734
+ const tree4 = {
735
+ id: 1,
736
+ name: 'node1',
737
+ children: [
738
+ { id: 2, name: 'node2', children: [] },
739
+ { id: 3, name: 'node3' }, // no children field
740
+ ],
741
+ };
742
+ const isEmpty4 = t.isEmptySingleTreeData(tree4)
743
+ console.log(isEmpty4) // false (because there are child nodes, even if they are empty)
744
+
745
+ // Support custom field names
746
+ const customTree = {
747
+ nodeId: 1,
748
+ name: 'node1',
749
+ subNodes: [],
750
+ };
751
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
752
+ const isEmptyCustom = t.isEmptySingleTreeData(customTree, fieldNames)
753
+ console.log(isEmptyCustom) // true
754
+ ```
755
+
756
+ ### isTreeData
757
+
758
+ Check if data is tree-structured data (array). Tree-structured data must be an array, and each element in the array must be valid single tree-structured data.
759
+
760
+ ```javascript
761
+ // Valid tree-structured data (forest)
762
+ const forest = [
763
+ {
764
+ id: 1,
765
+ name: 'node1',
766
+ children: [{ id: 2, name: 'node2' }],
767
+ },
768
+ {
769
+ id: 3,
770
+ name: 'node3',
771
+ children: [{ id: 4, name: 'node4' }],
772
+ },
773
+ ];
774
+ console.log(t.isTreeData(forest)) // true
775
+
776
+ // Empty array is also valid tree-structured data (empty forest)
777
+ console.log(t.isTreeData([])) // true
778
+
779
+ // Single object is not tree-structured data (should use isSingleTreeData)
780
+ console.log(t.isTreeData({ id: 1 })) // false
781
+
782
+ // Array contains non-tree-structured elements, returns false
783
+ const invalidForest = [
784
+ { id: 1, children: [{ id: 2 }] },
785
+ 'not a tree', // invalid element
786
+ ];
787
+ console.log(t.isTreeData(invalidForest)) // false
788
+
789
+ // null or undefined are not valid tree-structured data
790
+ console.log(t.isTreeData(null)) // false
791
+ console.log(t.isTreeData(undefined)) // false
792
+
793
+ // Support custom field names
794
+ const customForest = [
795
+ {
796
+ nodeId: 1,
797
+ name: 'node1',
798
+ subNodes: [{ nodeId: 2, name: 'node2' }],
799
+ },
800
+ ];
801
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
802
+ console.log(t.isTreeData(customForest, fieldNames)) // true
803
+ ```
804
+
805
+ ### isSingleTreeData
806
+
807
+ Check if data is single tree-structured data (single object). Tree-structured data must be an object (cannot be an array, null, undefined, or primitive type). If a children field exists, it must be an array type, and all child nodes will be recursively checked.
808
+
809
+ ```javascript
810
+ // Valid single tree-structured data
811
+ const tree = {
812
+ id: 1,
813
+ name: 'node1',
814
+ children: [
815
+ { id: 2, name: 'node2' },
816
+ { id: 3, name: 'node3' },
817
+ ],
818
+ };
819
+ const isValid = t.isSingleTreeData(tree)
820
+ console.log(isValid) // true
821
+
822
+ // No children field is also valid (only root node)
823
+ const singleNode = { id: 1, name: 'node1' }
824
+ console.log(t.isSingleTreeData(singleNode)) // true
825
+
826
+ // Array is not single tree-structured data
827
+ console.log(t.isSingleTreeData([])) // false
828
+
829
+ // null or undefined are not valid tree-structured data
830
+ console.log(t.isSingleTreeData(null)) // false
831
+ console.log(t.isSingleTreeData(undefined)) // false
832
+
833
+ // children cannot be null
834
+ const invalidTree = { id: 1, children: null }
835
+ console.log(t.isSingleTreeData(invalidTree)) // false
836
+
837
+ // Support custom field names
838
+ const customTree = {
839
+ nodeId: 1,
840
+ name: 'node1',
841
+ subNodes: [{ nodeId: 2, name: 'node2' }],
842
+ };
843
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
844
+ console.log(t.isSingleTreeData(customTree, fieldNames)) // true
845
+ ```
846
+
847
+ ### isValidTreeNode
848
+
849
+ Check if a single node is a valid tree node structure (lightweight, doesn't recursively check child nodes). Only checks the structure of the node itself, doesn't check child nodes.
850
+
851
+ ```javascript
852
+ // Valid tree node (has children array)
853
+ const node1 = {
854
+ id: 1,
855
+ name: 'node1',
856
+ children: [{ id: 2 }],
857
+ };
858
+ console.log(t.isValidTreeNode(node1)) // true
859
+
860
+ // Valid tree node (no children field)
861
+ const node2 = { id: 1, name: 'node1' };
862
+ console.log(t.isValidTreeNode(node2)) // true
863
+
864
+ // Invalid tree node (children is not an array)
865
+ const invalidNode = {
866
+ id: 1,
867
+ children: 'not an array',
868
+ };
869
+ console.log(t.isValidTreeNode(invalidNode)) // false
870
+
871
+ // Support custom field names
872
+ const customNode = {
873
+ nodeId: 1,
874
+ subNodes: [{ nodeId: 2 }],
875
+ };
876
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
877
+ console.log(t.isValidTreeNode(customNode, fieldNames)) // true
878
+ ```
879
+
880
+ **Difference from isSingleTreeData:**
881
+ - `isValidTreeNode` - Only checks the basic structure of a single node, doesn't recursively check child nodes (lightweight)
882
+ - `isSingleTreeData` - Recursively checks the entire tree structure, ensuring all child nodes are valid tree structures
883
+
884
+ ### isTreeNodeWithCircularCheck
885
+
886
+ Check if a node is a valid tree node structure and detect circular references. Uses WeakSet to track visited nodes, returns false if circular references are found.
887
+
888
+ ```javascript
889
+ // Valid tree node (no circular references)
890
+ const validNode = {
891
+ id: 1,
892
+ children: [
893
+ { id: 2, children: [{ id: 3 }] },
894
+ ],
895
+ };
896
+ console.log(t.isTreeNodeWithCircularCheck(validNode)) // true
897
+
898
+ // Detect circular references
899
+ const node1 = { id: 1, children: [] };
900
+ const node2 = { id: 2, children: [] };
901
+ node1.children.push(node2);
902
+ node2.children.push(node1); // circular reference
903
+ console.log(t.isTreeNodeWithCircularCheck(node1)) // false
904
+
905
+ // Detect self-reference
906
+ const selfRefNode = { id: 1, children: [] };
907
+ selfRefNode.children.push(selfRefNode); // self-reference
908
+ console.log(t.isTreeNodeWithCircularCheck(selfRefNode)) // false
909
+
910
+ // Support custom field names
911
+ const customNode = {
912
+ nodeId: 1,
913
+ subNodes: [{ nodeId: 2 }],
914
+ };
915
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
916
+ console.log(t.isTreeNodeWithCircularCheck(customNode, fieldNames)) // true
917
+ ```
918
+
919
+ **Use Cases:**
920
+ - Check for circular references when receiving user input or external data
921
+ - Data validation to prevent infinite recursion
922
+ - Check if data structure is correct during debugging
923
+
924
+ ### isSafeTreeDepth
925
+
926
+ Check if the depth of tree-structured data is safe (prevent recursion stack overflow). Returns false if the tree depth exceeds `maxDepth`.
927
+
928
+ ```javascript
929
+ // Safe depth tree
930
+ const safeTree = [
931
+ {
932
+ id: 1,
933
+ children: [
934
+ { id: 2, children: [{ id: 3 }] },
935
+ ],
936
+ },
937
+ ];
938
+ console.log(t.isSafeTreeDepth(safeTree, 10)) // true (depth is 3, less than 10)
939
+
940
+ // Depth exceeds maximum depth
941
+ const deepTree = [
942
+ {
943
+ id: 1,
944
+ children: [
945
+ { id: 2, children: [{ id: 3 }] },
946
+ ],
947
+ },
948
+ ];
949
+ console.log(t.isSafeTreeDepth(deepTree, 2)) // false (depth is 3, exceeds 2)
950
+
951
+ // Empty tree is always safe
952
+ console.log(t.isSafeTreeDepth([], 10)) // true
953
+
954
+ // Single layer tree
955
+ const singleLayer = [{ id: 1 }, { id: 2 }];
956
+ console.log(t.isSafeTreeDepth(singleLayer, 1)) // true
957
+
958
+ // Support custom field names
959
+ const customTree = [
960
+ {
961
+ nodeId: 1,
962
+ subNodes: [
963
+ { nodeId: 2, subNodes: [{ nodeId: 3 }] },
964
+ ],
965
+ },
966
+ ];
967
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
968
+ console.log(t.isSafeTreeDepth(customTree, 3, fieldNames)) // true
969
+ console.log(t.isSafeTreeDepth(customTree, 2, fieldNames)) // false
970
+ ```
971
+
972
+ **Use Cases:**
973
+ - Check if depth is safe before processing large trees
974
+ - Prevent recursion call stack overflow
975
+ - Performance optimization, avoid processing trees that are too deep
976
+
977
+ ## Custom Field Names
978
+
979
+ All methods support custom property names for children and id through the last parameter, passing in a configuration object:
980
+
981
+ ```javascript
982
+ // Use default field names
983
+ const foundNode1 = t.findTree(treeData, (node) => node.id === 2)
984
+
985
+ // Use custom field names
986
+ const fieldNames = { children: 'subNodes', id: 'nodeId' };
987
+ const foundNode2 = t.findTree(customTreeData, (node) => node.nodeId === 2, fieldNames);
988
+ ```
989
+
990
+ **Note:** All 30 functions support the `fieldNames` parameter to maintain API consistency. Even if the parameter has no effect in certain functions (such as `isEmptyTreeData`), it can still be passed to maintain consistent code style.
991
+
992
+ ## Testing
993
+
994
+ ### Run Tests
995
+
996
+ ```bash
997
+ # Run all tests
998
+ npm test
999
+
1000
+ # Run tests and generate coverage report
1001
+ npm run test:coverage
1002
+
1003
+ # Run tests (once, don't watch for file changes)
1004
+ npm test -- --run
1005
+ ```
1006
+
1007
+ ## Development
1008
+
1009
+ ```bash
1010
+ # Install dependencies
1011
+ npm install
1012
+
1013
+ # Run tests
1014
+ npm test
1015
+
1016
+ # Build project
1017
+ npm run build
1018
+ ```
1019
+
1020
+ <div align="center">
1021
+
1022
+ If this project helps you, please give it a ⭐️
1023
+
1024
+ Made with by [knott11]
1025
+
1026
+ </div>