path-treeify 1.3.0-beta.d47c4d9 → 1.4.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 +31 -9
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/types/dev/index.d.ts +1 -0
- package/dist/types/index.d.ts +57 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,10 +51,12 @@
|
|
|
51
51
|
- 🔗 Each node carries a `parent` circular reference for upward traversal
|
|
52
52
|
- 📍 Each node exposes a `getPath()` method to retrieve its own paths directly
|
|
53
53
|
- 🏷️ Each node has a `type` field — `PathTreeNodeKind.Dir` or `PathTreeNodeKind.File`
|
|
54
|
+
- 📏 Each node has a `depth` field indicating its distance from the root
|
|
54
55
|
- 👁️ `fileVisible` option includes files as leaf nodes alongside directories
|
|
55
56
|
- 🔍 Optional `filter` callback applied at **every depth**, including top-level entries
|
|
56
57
|
- ⚡ `build()` scans the entire `base` directory with zero configuration
|
|
57
58
|
- 🎛️ `buildBy()` accepts either a path segment array or a top-level filter function
|
|
59
|
+
- 🗃️ `usePathCache` option caches `getPath()` results per node for repeated access
|
|
58
60
|
- 📦 Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
|
|
59
61
|
- 🚫 Zero runtime dependencies
|
|
60
62
|
|
|
@@ -94,12 +96,12 @@ const treeifyWithFiles = new PathTreeify({
|
|
|
94
96
|
});
|
|
95
97
|
const fullTree = treeifyWithFiles.build();
|
|
96
98
|
|
|
97
|
-
// Check node type
|
|
99
|
+
// Check node type and depth
|
|
98
100
|
for (const child of tree.children) {
|
|
99
101
|
if (child.type === PathTreeNodeKind.Dir) {
|
|
100
|
-
console.log(
|
|
102
|
+
console.log(`dir (depth ${child.depth}):`, child.value);
|
|
101
103
|
} else if (child.type === PathTreeNodeKind.File) {
|
|
102
|
-
console.log(
|
|
104
|
+
console.log(`file (depth ${child.depth}):`, child.value);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
```
|
|
@@ -112,11 +114,12 @@ for (const child of tree.children) {
|
|
|
112
114
|
|
|
113
115
|
Creates a new instance.
|
|
114
116
|
|
|
115
|
-
| Option
|
|
116
|
-
|
|
117
|
-
| `base`
|
|
118
|
-
| `filter`
|
|
119
|
-
| `fileVisible`
|
|
117
|
+
| Option | Type | Required | Description |
|
|
118
|
+
|----------------|------------------------------|----------|-------------------------------------------------------------------------------------|
|
|
119
|
+
| `base` | `string` | ✅ | Absolute path to the root directory to scan from |
|
|
120
|
+
| `filter` | `FilterFunction` (see below) | ❌ | Applied at **every depth** — top-level entries included |
|
|
121
|
+
| `fileVisible` | `boolean` | ❌ | When `true`, files are included as leaf nodes. Defaults to `false` |
|
|
122
|
+
| `usePathCache` | `boolean` | ❌ | When `true`, `getPath()` results are cached on each node after the first call |
|
|
120
123
|
|
|
121
124
|
`base` must exist and be a directory, otherwise the constructor throws.
|
|
122
125
|
|
|
@@ -190,6 +193,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
|
|
|
190
193
|
|
|
191
194
|
```ts
|
|
192
195
|
interface PathTreeNode {
|
|
196
|
+
depth: number; // distance from root; root is 0, its children are 1, etc.
|
|
193
197
|
parent: PathTreeNode | null; // null only on the synthetic root
|
|
194
198
|
value: string; // entry name for this node
|
|
195
199
|
children: PathTreeNode[]; // empty for file nodes
|
|
@@ -199,6 +203,8 @@ interface PathTreeNode {
|
|
|
199
203
|
}
|
|
200
204
|
```
|
|
201
205
|
|
|
206
|
+
When `usePathCache: true` is set on the `PathTreeify` instance, the result of `getPath()` is cached on the node after the first call — subsequent calls return the same object reference without recomputing the parent chain.
|
|
207
|
+
|
|
202
208
|
> ⚠️ **Circular references** — `parent` points back up the tree. Use `JSON.stringify` replacers or a library like `flatted` if you need to serialize the result.
|
|
203
209
|
|
|
204
210
|
---
|
|
@@ -266,7 +272,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
|
|
|
266
272
|
function printPaths(node) {
|
|
267
273
|
for (const child of node.children) {
|
|
268
274
|
const { absolute } = child.getPath();
|
|
269
|
-
console.log(`[${child.type}] ${absolute}`);
|
|
275
|
+
console.log(`[${child.type}] depth=${child.depth} ${absolute}`);
|
|
270
276
|
printPaths(child);
|
|
271
277
|
}
|
|
272
278
|
}
|
|
@@ -274,6 +280,22 @@ function printPaths(node) {
|
|
|
274
280
|
printPaths(tree);
|
|
275
281
|
```
|
|
276
282
|
|
|
283
|
+
### Cache `getPath()` results for repeated traversal
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
const treeify = new PathTreeify({
|
|
287
|
+
base: '/your/project',
|
|
288
|
+
usePathCache: true,
|
|
289
|
+
});
|
|
290
|
+
const tree = treeify.build();
|
|
291
|
+
|
|
292
|
+
// First call walks the parent chain and caches the result
|
|
293
|
+
const pathA = tree.children[0].getPath();
|
|
294
|
+
// Subsequent calls return the cached object directly
|
|
295
|
+
const pathB = tree.children[0].getPath();
|
|
296
|
+
console.log(pathA === pathB); // true
|
|
297
|
+
```
|
|
298
|
+
|
|
277
299
|
### CommonJS usage
|
|
278
300
|
|
|
279
301
|
```js
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,t=require("fs"),i=require("path");class r{static isValid(e){try{return t.accessSync(e,t.constants.F_OK),!0}catch{return!1}}static isDirectory(e){try{return t.statSync(e).isDirectory()}catch{return!1}}static isFile(e){try{return t.statSync(e).isFile()}catch{return!1}}}exports.PathTreeNodeKind=void 0,(e=exports.PathTreeNodeKind||(exports.PathTreeNodeKind={})).Dir="dir",e.File="file",e.Unknown="unknown";class s{constructor(e){this.
|
|
1
|
+
"use strict";var e,t=require("fs"),i=require("path");class r{static isValid(e){try{return t.accessSync(e,t.constants.F_OK),!0}catch{return!1}}static isDirectory(e){try{return t.statSync(e).isDirectory()}catch{return!1}}static isFile(e){try{return t.statSync(e).isFile()}catch{return!1}}}exports.PathTreeNodeKind=void 0,(e=exports.PathTreeNodeKind||(exports.PathTreeNodeKind={})).Dir="dir",e.File="file",e.Unknown="unknown";class s{constructor(e){this.usePathCache=!1,this.base=e.base,"boolean"==typeof e.usePathCache&&(this.usePathCache=e.usePathCache)}getPath(){let e=this;const t=()=>{let t="",r=this;for(;r.parent;)t=t?`${r.value}${i.sep}${t}`:r.value,r=r.parent;return{relative:t,absolute:i.resolve(e.base,t)}};return e.usePathCache?(e._pathCache||(e._pathCache=t()),e._pathCache):t()}}exports.PathTreeify=class{constructor(e){this.fileVisible=!1;const{filter:t,base:i,fileVisible:n,usePathCache:a}=e;if("boolean"==typeof n&&n&&(this.fileVisible=n),void 0!==t&&(this.validateFilter(t),this.userFilter=t),!i||!r.isValid(i))throw new Error(`${i} is not a valid path!`);if(!r.isDirectory(i))throw new Error(`${i} is not a dirPath!`);this.base=i,this.pathTreeNodeShared=new s({base:i,usePathCache:a})}applyFilter(e,t){return!(!this.fileVisible&&!r.isDirectory(e))&&(!this.userFilter||this.userFilter({name:t,dirPath:i.dirname(e)}))}validateFilter(e){if("function"!=typeof e)throw new TypeError("filter must be a function")}initNode(){const e=Object.create(this.pathTreeNodeShared),t=Object.create(e);return t.parent=null,t.value="",t.children=[],t.type=exports.PathTreeNodeKind.Unknown,t.depth=-1,t}buildChildren(e,s,n){const a=[],o=n||t.readdirSync(e),h=s.depth+1;for(const t of o){const n=i.join(e,t);if(!this.applyFilter(n,t))continue;const o=this.initNode();o.depth=h,o.value=t,o.parent=s,a.push(o),this.fileVisible&&r.isFile(n)?o.type=exports.PathTreeNodeKind.File:(o.type=exports.PathTreeNodeKind.Dir,o.children=this.buildChildren(n,o))}return a}checkRelativePaths(e){for(let t=0;t<e.length;t++){const s=e[t];if("string"!=typeof s)throw new Error(`Item at index ${t} is not a string, got ${typeof s}`);const n=i.resolve(this.base,s);if(!r.isValid(n))throw new Error(`Path does not exist or is not accessible: ${n} (from relative path: ${s})`);if(!(r.isDirectory(n)||this.fileVisible&&r.isFile(n)))throw new Error(`Path is not a directory: ${n} (from relative path: ${s})`)}}formatSegments(e){return e.map(e=>e.split(/[/\\]/).filter(Boolean).join(i.sep)).filter(Boolean)}getAllEntriesUnderBase(){return t.readdirSync(this.base).filter(e=>{const t=i.resolve(this.base,e);return this.applyFilter(t,e)})}buildBySegments(e){const t=this.initNode();return t.depth=0,t.children=this.buildChildren(this.base,t,e),t}buildByFilter(e){const t=this.getAllEntriesUnderBase();return this.buildBySegments(t.filter(e))}buildBy(e){if(Array.isArray(e)){const t=this.formatSegments(e);return this.checkRelativePaths(t),this.buildBySegments(t)}if("function"==typeof e)return this.buildByFilter(e);throw new TypeError("buildBy: expected an array of strings or a filter function, but received "+typeof e)}build(){const e=this.getAllEntriesUnderBase();return this.buildBySegments(e)}};
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readdirSync as t,accessSync as
|
|
1
|
+
import{readdirSync as t,accessSync as e,constants as i,statSync as r}from"fs";import{dirname as s,join as n,resolve as a,sep as h}from"path";class l{static isValid(t){try{return e(t,i.F_OK),!0}catch{return!1}}static isDirectory(t){try{return r(t).isDirectory()}catch{return!1}}static isFile(t){try{return r(t).isFile()}catch{return!1}}}var o;!function(t){t.Dir="dir",t.File="file",t.Unknown="unknown"}(o||(o={}));class c{constructor(t){this.usePathCache=!1,this.base=t.base,"boolean"==typeof t.usePathCache&&(this.usePathCache=t.usePathCache)}getPath(){let t=this;const e=()=>{let e="",i=this;for(;i.parent;)e=e?`${i.value}${h}${e}`:i.value,i=i.parent;return{relative:e,absolute:a(t.base,e)}};return t.usePathCache?(t._pathCache||(t._pathCache=e()),t._pathCache):e()}}class u{constructor(t){this.fileVisible=!1;const{filter:e,base:i,fileVisible:r,usePathCache:s}=t;if("boolean"==typeof r&&r&&(this.fileVisible=r),void 0!==e&&(this.validateFilter(e),this.userFilter=e),!i||!l.isValid(i))throw new Error(`${i} is not a valid path!`);if(!l.isDirectory(i))throw new Error(`${i} is not a dirPath!`);this.base=i,this.pathTreeNodeShared=new c({base:i,usePathCache:s})}applyFilter(t,e){return!(!this.fileVisible&&!l.isDirectory(t))&&(!this.userFilter||this.userFilter({name:e,dirPath:s(t)}))}validateFilter(t){if("function"!=typeof t)throw new TypeError("filter must be a function")}initNode(){const t=Object.create(this.pathTreeNodeShared),e=Object.create(t);return e.parent=null,e.value="",e.children=[],e.type=o.Unknown,e.depth=-1,e}buildChildren(e,i,r){const s=[],a=r||t(e),h=i.depth+1;for(const t of a){const r=n(e,t);if(!this.applyFilter(r,t))continue;const a=this.initNode();a.depth=h,a.value=t,a.parent=i,s.push(a),this.fileVisible&&l.isFile(r)?a.type=o.File:(a.type=o.Dir,a.children=this.buildChildren(r,a))}return s}checkRelativePaths(t){for(let e=0;e<t.length;e++){const i=t[e];if("string"!=typeof i)throw new Error(`Item at index ${e} is not a string, got ${typeof i}`);const r=a(this.base,i);if(!l.isValid(r))throw new Error(`Path does not exist or is not accessible: ${r} (from relative path: ${i})`);if(!(l.isDirectory(r)||this.fileVisible&&l.isFile(r)))throw new Error(`Path is not a directory: ${r} (from relative path: ${i})`)}}formatSegments(t){return t.map(t=>t.split(/[/\\]/).filter(Boolean).join(h)).filter(Boolean)}getAllEntriesUnderBase(){return t(this.base).filter(t=>{const e=a(this.base,t);return this.applyFilter(e,t)})}buildBySegments(t){const e=this.initNode();return e.depth=0,e.children=this.buildChildren(this.base,e,t),e}buildByFilter(t){const e=this.getAllEntriesUnderBase();return this.buildBySegments(e.filter(t))}buildBy(t){if(Array.isArray(t)){const e=this.formatSegments(t);return this.checkRelativePaths(e),this.buildBySegments(e)}if("function"==typeof t)return this.buildByFilter(t);throw new TypeError("buildBy: expected an array of strings or a filter function, but received "+typeof t)}build(){const t=this.getAllEntriesUnderBase();return this.buildBySegments(t)}}export{o as PathTreeNodeKind,u as PathTreeify};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ interface PathTreeifyProps {
|
|
|
11
11
|
filter?: FilterFunction;
|
|
12
12
|
/** When true, files are included as leaf nodes alongside directories. Defaults to false */
|
|
13
13
|
fileVisible?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* When true, the result of {@link PathTreeNode.getPath} is memoised on each node
|
|
16
|
+
* after the first call. Subsequent calls return the same object reference without
|
|
17
|
+
* re-walking the parent chain. Useful when nodes are accessed repeatedly.
|
|
18
|
+
* Defaults to false.
|
|
19
|
+
*/
|
|
20
|
+
usePathCache?: boolean;
|
|
14
21
|
}
|
|
15
22
|
/** Classification of a node in the path tree */
|
|
16
23
|
export declare enum PathTreeNodeKind {
|
|
@@ -19,11 +26,28 @@ export declare enum PathTreeNodeKind {
|
|
|
19
26
|
/** Assigned before the node's type has been resolved */
|
|
20
27
|
Unknown = "unknown"
|
|
21
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Public interface for a node in the path tree.
|
|
31
|
+
* Consumers receive this type; the internal implementation class is not exported.
|
|
32
|
+
*/
|
|
22
33
|
export interface PathTreeNode {
|
|
34
|
+
/** Distance from the root node; root itself is 0, its direct children are 1, and so on */
|
|
35
|
+
depth: number;
|
|
36
|
+
/** Reference to the parent node; null for the root node */
|
|
23
37
|
parent: PathTreeNode | null;
|
|
38
|
+
/** The entry name of this node (not a full path) */
|
|
24
39
|
value: string;
|
|
40
|
+
/** Child nodes; non-empty only for directory nodes */
|
|
25
41
|
children: PathTreeNode[];
|
|
42
|
+
/** Whether this node is a directory, a file, or not yet resolved */
|
|
26
43
|
type: PathTreeNodeKind;
|
|
44
|
+
/**
|
|
45
|
+
* Walks up the parent chain to compute this node's relative and absolute paths.
|
|
46
|
+
* When the owning {@link PathTreeify} instance was created with `usePathCache: true`,
|
|
47
|
+
* the result is memoised after the first call and the same object is returned on
|
|
48
|
+
* every subsequent call.
|
|
49
|
+
* @returns `relative` — path from the tree root; `absolute` — fully resolved path on disk
|
|
50
|
+
*/
|
|
27
51
|
getPath(): {
|
|
28
52
|
relative: string;
|
|
29
53
|
absolute: string;
|
|
@@ -33,6 +57,12 @@ export interface PathTreeNode {
|
|
|
33
57
|
export declare class PathTreeify {
|
|
34
58
|
/** The root directory to scan */
|
|
35
59
|
private base;
|
|
60
|
+
/**
|
|
61
|
+
* Shared prototype instance for nodes produced by this builder.
|
|
62
|
+
* All nodes created via {@link initNode} inherit `base` and `getPath` from this object,
|
|
63
|
+
* avoiding per-node storage of the base path string.
|
|
64
|
+
*/
|
|
65
|
+
private pathTreeNodeShared;
|
|
36
66
|
/**
|
|
37
67
|
* Optional user-supplied filter. When set, every entry must pass this predicate
|
|
38
68
|
* in addition to the built-in visibility check.
|
|
@@ -40,6 +70,7 @@ export declare class PathTreeify {
|
|
|
40
70
|
private userFilter?;
|
|
41
71
|
/** When true, files are included as leaf nodes during traversal. Defaults to false */
|
|
42
72
|
private fileVisible;
|
|
73
|
+
constructor(props: Partial<PathTreeifyProps>);
|
|
43
74
|
/**
|
|
44
75
|
* Determines whether a given entry should be included in the tree.
|
|
45
76
|
* - If {@link fileVisible} is false, non-directory entries are always excluded.
|
|
@@ -48,27 +79,39 @@ export declare class PathTreeify {
|
|
|
48
79
|
* @param name - Entry name (filename or directory name)
|
|
49
80
|
*/
|
|
50
81
|
private applyFilter;
|
|
51
|
-
constructor({ filter, base, fileVisible }: Partial<PathTreeifyProps>);
|
|
52
82
|
/**
|
|
53
83
|
* Asserts that the provided value is a callable {@link FilterFunction}.
|
|
54
84
|
* Throws a TypeError if the check fails.
|
|
55
85
|
*/
|
|
56
86
|
private validateFilter;
|
|
57
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Creates a new unattached {@link PathTreeNode}.
|
|
89
|
+
* The node is created with a two-layer prototype chain:
|
|
90
|
+
* `node → cache → pathTreeNodeShared`. The intermediate `cache` layer is a
|
|
91
|
+
* per-node object that holds `_pathCache` when `usePathCache` is enabled,
|
|
92
|
+
* keeping the cached value isolated to each node while still inheriting `base`
|
|
93
|
+
* and `getPath` from `pathTreeNodeShared`.
|
|
94
|
+
* `depth` is initialised to `-1` and must be set by the caller.
|
|
95
|
+
*/
|
|
58
96
|
private initNode;
|
|
59
97
|
/**
|
|
60
98
|
* Recursively reads {@link dirPath} and builds child nodes for each entry that
|
|
61
99
|
* passes {@link applyFilter}. Directories are traversed depth-first;
|
|
62
100
|
* files (when {@link fileVisible} is true) become leaf nodes.
|
|
63
|
-
*
|
|
64
|
-
* @param
|
|
101
|
+
*
|
|
102
|
+
* @param dirPath - Absolute path of the directory to read
|
|
103
|
+
* @param parent - The parent node to attach child nodes to
|
|
104
|
+
* @param segments - Optional explicit list of entry names to use instead of reading the
|
|
105
|
+
* directory from disk; used by {@link buildBySegments} to skip a
|
|
106
|
+
* redundant `readdirSync` when the segment list is already known
|
|
65
107
|
*/
|
|
66
108
|
private buildChildren;
|
|
67
109
|
/**
|
|
68
110
|
* Validates that every entry in {@link relativeSegments} refers to an accessible
|
|
69
111
|
* path under {@link base}. When {@link fileVisible} is false, each path must be
|
|
70
112
|
* a directory; when true, regular files are also accepted.
|
|
71
|
-
* @param relativeSegments - Relative path strings to validate
|
|
113
|
+
* @param relativeSegments - Relative path strings to validate; assumed to be a string
|
|
114
|
+
* array (callers are responsible for type safety at the boundary)
|
|
72
115
|
*/
|
|
73
116
|
private checkRelativePaths;
|
|
74
117
|
/**
|
|
@@ -84,21 +127,23 @@ export declare class PathTreeify {
|
|
|
84
127
|
*/
|
|
85
128
|
private getAllEntriesUnderBase;
|
|
86
129
|
/**
|
|
87
|
-
* Builds a subtree
|
|
88
|
-
*
|
|
89
|
-
* @
|
|
130
|
+
* Builds a subtree whose top-depth children correspond to {@link segments}.
|
|
131
|
+
* The root node is created at depth 0; children are built by delegating to
|
|
132
|
+
* {@link buildChildren}, passing {@link segments} directly to avoid a redundant
|
|
133
|
+
* `readdirSync` of the base directory.
|
|
134
|
+
* @param segments - Normalised and validated relative path segments
|
|
90
135
|
*/
|
|
91
136
|
private buildBySegments;
|
|
92
137
|
/**
|
|
93
|
-
* Builds a subtree from top-
|
|
94
|
-
* Note: this predicate only affects top-
|
|
138
|
+
* Builds a subtree from top-depth entries whose names satisfy {@link filter}.
|
|
139
|
+
* Note: this predicate only affects top-depth selection, not recursive traversal.
|
|
95
140
|
* For recursive filtering use the `filter` constructor option.
|
|
96
|
-
* @param filter - Predicate applied to each top-
|
|
141
|
+
* @param filter - Predicate applied to each top-depth entry name
|
|
97
142
|
*/
|
|
98
143
|
private buildByFilter;
|
|
99
144
|
/** Overload: build the tree from an explicit list of relative path segments */
|
|
100
145
|
buildBy(segments: string[]): PathTreeNode;
|
|
101
|
-
/** Overload: build the tree from a predicate applied to top-
|
|
146
|
+
/** Overload: build the tree from a predicate applied to top-depth entry names */
|
|
102
147
|
buildBy(filter: (segment: string) => boolean): PathTreeNode;
|
|
103
148
|
/** Builds a full tree from all immediate entries under the base path */
|
|
104
149
|
build(): PathTreeNode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "path-treeify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Convert a path or an array of paths into a tree-structured JavaScript object, where each node has a `parent` property that holds a circular reference to its parent node.",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|