path-treeify 1.3.0 β†’ 1.4.0-beta.31d8864

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
@@ -1,4 +1,7 @@
1
- # path-treeify
1
+ <div align="center">
2
+ <img alt="LOGO" width="320" src="https://assets-amu.pages.dev/path-treeify/logo.png">
3
+ </div>
4
+ <br/>
2
5
 
3
6
  > πŸ“– [δΈ­ζ–‡ζ–‡ζ‘£ (Chinese README)](https://github.com/isaaxite/path-treeify/blob/main/docs/README.zh-CN.md)
4
7
 
@@ -14,6 +17,9 @@
14
17
  <a href="https://nodejs.org">
15
18
  <img alt="node" src="https://img.shields.io/node/v/path-treeify">
16
19
  </a>
20
+ <a href="https://github.com/isaaxite/path-treeify/blob/main/CHANGELOG.md">
21
+ <img alt="CHANGELOG" src="https://img.shields.io/badge/changelog-maintained-brightgreen">
22
+ </a>
17
23
  <a href="https://github.com/isaaxite/path-treeify/blob/main/LICENSE">
18
24
  <img alt="GitHub License" src="https://img.shields.io/github/license/isaaxite/path-treeify">
19
25
  </a>
@@ -21,7 +27,7 @@
21
27
  <img alt="GitHub Created At" src="https://img.shields.io/github/created-at/isaaxite/path-treeify">
22
28
  </a>
23
29
  <a href="https://github.com/isaaxite/path-treeify">
24
- <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/isaaxite/path-treeify">
30
+ <img alt="NPM Unpacked Size" src="https://img.shields.io/npm/unpacked-size/path-treeify">
25
31
  </a>
26
32
  <a href="https://github.com/isaaxite/path-treeify/commits/main/">
27
33
  <img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/isaaxite/path-treeify">
@@ -29,8 +35,8 @@
29
35
  <a href="https://github.com/isaaxite/path-treeify/commits/main/">
30
36
  <img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/isaaxite/path-treeify">
31
37
  </a>
32
- <a href='https://codecov.io/github/isaaxite/path-treeify/tests'>
33
- <img src='https://github.com/isaaxite/path-treeify/actions/workflows/unittests.yml/badge.svg' alt='Coverage Status' />
38
+ <a href='https://github.com/isaaxite/path-treeify/actions/workflows/unittests.yml'>
39
+ <img src='https://github.com/isaaxite/path-treeify/actions/workflows/unittests.yml/badge.svg' alt='Test CI Status' />
34
40
  </a>
35
41
  <a href='https://coveralls.io/github/isaaxite/path-treeify'>
36
42
  <img src='https://coveralls.io/repos/github/isaaxite/path-treeify/badge.svg' alt='Coverage Status' />
@@ -44,9 +50,13 @@
44
50
  - 🌲 Builds a recursive tree from one or more directory paths
45
51
  - πŸ”— Each node carries a `parent` circular reference for upward traversal
46
52
  - πŸ“ Each node exposes a `getPath()` method to retrieve its own paths directly
47
- - πŸ” Optional `filter` callback applied at **every depth**, including top-level directories
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
55
+ - πŸ‘οΈ `fileVisible` option includes files as leaf nodes alongside directories
56
+ - πŸ” Optional `filter` callback applied at **every depth**, including top-level entries
48
57
  - ⚑ `build()` scans the entire `base` directory with zero configuration
49
- - πŸŽ›οΈ `buildBy()` accepts either a directory name array or a top-level filter function
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
50
60
  - πŸ“¦ Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
51
61
  - 🚫 Zero runtime dependencies
52
62
 
@@ -73,18 +83,27 @@ yarn add path-treeify
73
83
  ## Quick Start
74
84
 
75
85
  ```ts
76
- import { PathTreeify } from 'path-treeify';
86
+ import { PathTreeify, PathTreeNodeKind } from 'path-treeify';
77
87
 
88
+ // Directories only (default)
78
89
  const treeify = new PathTreeify({ base: '/your/project/root' });
90
+ const tree = treeify.build();
79
91
 
80
- // Scan specific directories by name
81
- const tree = treeify.buildBy(['src', 'tests']);
82
-
83
- // Scan with a top-level filter function
84
- const filtered = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
85
-
86
- // Or scan everything under base at once
87
- const fullTree = treeify.build();
92
+ // Include files as leaf nodes
93
+ const treeifyWithFiles = new PathTreeify({
94
+ base: '/your/project/root',
95
+ fileVisible: true,
96
+ });
97
+ const fullTree = treeifyWithFiles.build();
98
+
99
+ // Check node type and depth
100
+ for (const child of tree.children) {
101
+ if (child.type === PathTreeNodeKind.Dir) {
102
+ console.log(`dir (depth ${child.depth}):`, child.value);
103
+ } else if (child.type === PathTreeNodeKind.File) {
104
+ console.log(`file (depth ${child.depth}):`, child.value);
105
+ }
106
+ }
88
107
  ```
89
108
 
90
109
  ---
@@ -95,10 +114,12 @@ const fullTree = treeify.build();
95
114
 
96
115
  Creates a new instance.
97
116
 
98
- | Option | Type | Required | Description |
99
- |----------|-------------------------------|----------|-----------------------------------------------------------------------------|
100
- | `base` | `string` | βœ… | Absolute path to the root directory to scan from |
101
- | `filter` | `FilterFunction` (see below) | ❌ | Applied at **every depth** β€” top-level directories included |
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 |
102
123
 
103
124
  `base` must exist and be a directory, otherwise the constructor throws.
104
125
 
@@ -110,14 +131,14 @@ Used as the `filter` option in the constructor. Applied at every level of the tr
110
131
 
111
132
  ```ts
112
133
  type FilterFunction = (params: {
113
- name: string; // directory name (leaf segment)
134
+ name: string; // entry name (file or directory name)
114
135
  dirPath: string; // absolute path of the parent directory
115
136
  }) => boolean;
116
137
  ```
117
138
 
118
- Return `true` to **include** the directory and recurse into it; `false` to **skip** it entirely.
139
+ Return `true` to **include** the entry; `false` to **skip** it entirely.
119
140
 
120
- **Example β€” exclude `node_modules` and hidden directories at every depth:**
141
+ **Example β€” exclude `node_modules` and hidden entries at every depth:**
121
142
 
122
143
  ```ts
123
144
  const treeify = new PathTreeify({
@@ -130,7 +151,7 @@ const treeify = new PathTreeify({
130
151
 
131
152
  ### `build(): PathTreeNode`
132
153
 
133
- Scans all subdirectories directly under `base` (applying the instance-level `filter` if set) and returns a synthetic root `PathTreeNode`.
154
+ Scans all entries directly under `base` that pass the instance-level `filter` and returns a synthetic root `PathTreeNode`. When `fileVisible` is `true`, files are included as leaf nodes.
134
155
 
135
156
  ```ts
136
157
  const tree = treeify.build();
@@ -138,64 +159,73 @@ const tree = treeify.build();
138
159
 
139
160
  ---
140
161
 
141
- ### `buildBy(dirNames: string[]): PathTreeNode`
162
+ ### `buildBy(segments: string[]): PathTreeNode`
142
163
 
143
- Builds a tree from the given list of directory names (relative to `base`).
164
+ Builds a tree from the given list of relative path segments. When `fileVisible` is `true`, file paths are also accepted.
144
165
 
145
166
  ```ts
146
167
  const tree = treeify.buildBy(['src', 'docs', 'tests']);
168
+
169
+ // With fileVisible: true, files can also be specified
170
+ const treeWithFiles = new PathTreeify({ base: '/your/project', fileVisible: true });
171
+ treeWithFiles.buildBy(['src', 'README.md']);
147
172
  ```
148
173
 
149
- - Leading and trailing slashes are stripped automatically.
150
- - Throws if any name does not resolve to a valid directory under `base`.
174
+ - Both `/` and `\` separators are normalised automatically.
175
+ - Leading/trailing slashes and empty segments are stripped.
176
+ - Throws if any segment does not resolve to a valid entry under `base`.
151
177
 
152
- ### `buildBy(filter: (dirName: string) => boolean): PathTreeNode`
178
+ ### `buildBy(filter: (segment: string) => boolean): PathTreeNode`
153
179
 
154
- Collects all top-level subdirectories under `base`, applies the given predicate to select which ones to include, then builds a tree from the matching names. The instance-level `filter` still applies during deep traversal.
180
+ Collects all top-level entries under `base`, applies the predicate to select which ones to include, then builds a tree. The instance-level `filter` still applies during deep traversal.
155
181
 
156
182
  ```ts
157
183
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
158
184
  ```
159
185
 
160
- > **Note:** the predicate passed to `buildBy(fn)` only selects which **top-level** directories to include. To filter directories at every depth, pass a `filter` to the constructor.
186
+ > **Note:** the predicate passed to `buildBy(fn)` only selects which **top-level** entries to include. To filter entries at every depth, pass a `filter` to the constructor.
161
187
 
162
188
  ---
163
189
 
164
- ### `getPathBy(node: PathTreeNode): { relative: string; absolute: string }`
190
+ ### `PathTreeNode`
165
191
 
166
- Walks a node's `parent` chain to reconstruct its full path. Equivalent to calling `node.getPath()` directly.
192
+ `PathTreeNode` is an **interface** β€” each node exposes a `getPath()` method to retrieve its paths without needing the `PathTreeify` instance.
167
193
 
168
194
  ```ts
169
- const { relative, absolute } = treeify.getPathBy(node);
170
- // relative β†’ e.g. 'src/components'
171
- // absolute β†’ e.g. '/your/project/src/components'
195
+ interface PathTreeNode {
196
+ depth: number; // distance from root; root is 0, its children are 1, etc.
197
+ parent: PathTreeNode | null; // null only on the synthetic root
198
+ value: string; // entry name for this node
199
+ children: PathTreeNode[]; // empty for file nodes
200
+ type: PathTreeNodeKind; // Dir, File, or Unknown
201
+
202
+ getPath(): { relative: string; absolute: string };
203
+ }
172
204
  ```
173
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
+
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.
209
+
174
210
  ---
175
211
 
176
- ### `PathTreeNode`
212
+ ### `PathTreeNodeKind`
177
213
 
178
- `PathTreeNode` is a **class** with its own `getPath()` method, so you can retrieve a node's path without passing it back to the `PathTreeify` instance.
214
+ An enum classifying each node's filesystem type.
179
215
 
180
216
  ```ts
181
- class PathTreeNode {
182
- parent: PathTreeNode | null; // null only on the synthetic root
183
- value: string; // directory name for this node
184
- children: PathTreeNode[];
185
-
186
- getPath(): { relative: string; absolute: string };
217
+ enum PathTreeNodeKind {
218
+ Dir = 'dir',
219
+ File = 'file',
220
+ Unknown = 'unknown', // assigned before the type is resolved
187
221
  }
188
222
  ```
189
223
 
190
- `node.getPath()` returns the same result as `treeify.getPathBy(node)` β€” both are available for convenience.
191
-
192
- > ⚠️ **Circular references** β€” `parent` points back up the tree. Use `JSON.stringify` replacers or a library like `flatted` if you need to serialize the result.
193
-
194
224
  ---
195
225
 
196
226
  ## Examples
197
227
 
198
- ### Scan an entire base directory
228
+ ### Directories only (default)
199
229
 
200
230
  ```ts
201
231
  import { PathTreeify } from 'path-treeify';
@@ -204,6 +234,16 @@ const treeify = new PathTreeify({ base: '/your/project' });
204
234
  const tree = treeify.build();
205
235
  ```
206
236
 
237
+ ### Include files as leaf nodes
238
+
239
+ ```ts
240
+ const treeify = new PathTreeify({
241
+ base: '/your/project',
242
+ fileVisible: true,
243
+ });
244
+ const tree = treeify.build();
245
+ ```
246
+
207
247
  ### Exclude directories at every depth via constructor filter
208
248
 
209
249
  ```ts
@@ -214,13 +254,13 @@ const treeify = new PathTreeify({
214
254
  const tree = treeify.build();
215
255
  ```
216
256
 
217
- ### Scan specific directories
257
+ ### Scan specific paths
218
258
 
219
259
  ```ts
220
260
  const tree = treeify.buildBy(['src', 'tests', 'docs']);
221
261
  ```
222
262
 
223
- ### Select top-level directories with a predicate
263
+ ### Select top-level entries with a predicate
224
264
 
225
265
  ```ts
226
266
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
@@ -232,7 +272,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
232
272
  function printPaths(node) {
233
273
  for (const child of node.children) {
234
274
  const { absolute } = child.getPath();
235
- console.log(absolute);
275
+ console.log(`[${child.type}] depth=${child.depth} ${absolute}`);
236
276
  printPaths(child);
237
277
  }
238
278
  }
@@ -240,10 +280,26 @@ function printPaths(node) {
240
280
  printPaths(tree);
241
281
  ```
242
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
+
243
299
  ### CommonJS usage
244
300
 
245
301
  ```js
246
- const { PathTreeify } = require('path-treeify');
302
+ const { PathTreeify, PathTreeNodeKind } = require('path-treeify');
247
303
 
248
304
  const treeify = new PathTreeify({ base: __dirname });
249
305
  const tree = treeify.build();
@@ -253,4 +309,4 @@ const tree = treeify.build();
253
309
 
254
310
  ## License
255
311
 
256
- [MIT](https://github.com/isaaxite/path-treeify/blob/main/LICENSE) Β© [isaaxite](https://github.com/isaaxite)
312
+ [MIT](https://github.com/isaaxite/path-treeify/blob/main/LICENSE) Β© [isaaxite](https://github.com/isaaxite)
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var t=require("fs"),e=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}}}class i{constructor(t){this.parent=null,this.value="",this.children=[],this.base=t}getPath(){let t="",r=this;for(;r.parent;)t=t?`${r.value}${e.sep}${t}`:r.value,r=r.parent;return{relative:t,absolute:e.resolve(this.base,t)}}}exports.PathTreeify=class{constructor({filter:t,base:e}){if(void 0!==t&&(this.validateFilter(t),this.filter=t),!e||!r.isValid(e))throw new Error(`${e} is not a valid path!`);if(!r.isDirectory(e))throw new Error(`${e} is not a dirPath!`);this.base=e}validateFilter(t){if("function"!=typeof t)throw new TypeError("filter must be a function");if(1!==t.length)throw new TypeError("filter must accept exactly one parameter");try{if("boolean"!=typeof t({name:"test",postPath:"/test"}))throw new TypeError("filter must return a boolean")}catch(t){throw new TypeError("filter function threw an error during test: "+t)}}initNode(t=null){const e=new i(this.base);return t&&(e.parent=t),e}buildChildren(r,i){const s=t.readdirSync(r),a=[];for(const n of s){const s=e.join(r,n);if(!t.statSync(s).isDirectory())continue;if(this.filter&&!this.filter({dirPath:r,name:n}))continue;const o=this.initNode();o.value=n,o.parent=i,o.children=this.buildChildren(s,o),a.push(o)}return a}checkRelativePaths(t){if(!Array.isArray(t))throw new Error("Expected array, got "+typeof t);for(let i=0;i<t.length;i++){const s=t[i];if("string"!=typeof s)throw new Error(`Item at index ${i} is not a string, got ${typeof s}`);const a=e.resolve(this.base,s);if(!r.isValid(a))throw new Error(`Path does not exist or is not accessible: ${a} (from relative path: ${s})`);if(!r.isDirectory(a))throw new Error(`Path is not a directory: ${a} (from relative path: ${s})`)}}formatDirnames(t){return t.map(t=>t.replace(/^\/+|\/+$/g,"")).filter(t=>""!==t)}getAllDirNamesUnderBase(){return t.readdirSync(this.base).filter(t=>{const i=e.resolve(this.base,t);return!!r.isDirectory(i)&&!(this.filter&&!this.filter({name:t,dirPath:this.base}))})}buildByDirNames(t){const r=this.initNode(),i=this.formatDirnames(t);this.checkRelativePaths(i);for(const t of i){const i=this.initNode();i.value=t,i.parent=r,i.children=this.buildChildren(e.resolve(this.base,t),i),r.children.push(i)}return r}buildByFilter(t){const e=this.getAllDirNamesUnderBase();return this.buildByDirNames(e.filter(t))}getPathBy(t){let r="",i=t;for(;i.parent;)r=r?`${i.value}${e.sep}${r}`:i.value,i=i.parent;return{relative:r,absolute:e.resolve(this.base,r)}}buildBy(t){if(Array.isArray(t))return this.buildByDirNames(t);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.getAllDirNamesUnderBase();return this.buildByDirNames(t)}};
1
+ "use strict";var e,t=require("fs"),r=require("path");function i(e,t){for(const r in t)Object.defineProperty(e,r,{value:t[r],writable:!1,configurable:!1,enumerable:!1})}function s(e){try{return function(e){try{const r=t.statSync(e);return r.isDirectory()?exports.PathTreeNodeKind.Dir:r.isFile()?exports.PathTreeNodeKind.File:exports.PathTreeNodeKind.Other}catch(e){if("ENOENT"!==e.code)throw e}try{return t.lstatSync(e).isSymbolicLink()?exports.PathTreeNodeKind.BrokenSymlink:exports.PathTreeNodeKind.Other}catch(e){if("ENOENT"!==e.code)throw e}return exports.PathTreeNodeKind.NotFound}(e)}catch(e){return exports.PathTreeNodeKind.Error}}exports.PathTreeNodeKind=void 0,(e=exports.PathTreeNodeKind||(exports.PathTreeNodeKind={})).Dir="dir",e.File="file",e.Unknown="unknown",e.BrokenSymlink="broken_symlink",e.Other="other",e.NotFound="not_found",e.Error="error";class n{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}}}class o{constructor(e){this.depth=-1,this.parent=null,this.value="",this.children=[],this.type=exports.PathTreeNodeKind.Unknown,i(this,{_usePathCache:e.usePathCache})}getPath(){let e=this,t="";const s=()=>{let e="",i=this;for(;;){if(null===i.parent){t=i.value;break}e=e?`${i.value}${r.sep}${e}`:i.value,i=i.parent}return{relative:e,absolute:r.resolve(t,e)}};return e._usePathCache?(e._pathCache||i(this,{_pathCache:s()}),e._pathCache):s()}}exports.PathTreeify=class{constructor(e){this._base="",this._fileVisible=!1,this._usePathCache=!1;const{filter:t,base:r,fileVisible:s,usePathCache:o}=e;if(void 0!==t&&(this.validateFilter(t),i(this,{_userFilter:t})),!r||!n.isValid(r))throw new Error(`${r} is not a valid path!`);if(!n.isDirectory(r))throw new Error(`${r} is not a dirPath!`);i(this,{_usePathCache:Boolean(o),_base:r,_fileVisible:!("boolean"!=typeof s||!s)&&s})}applyFilter(e,t){return!(!this._fileVisible&&!n.isDirectory(e))&&(!this._userFilter||this._userFilter({name:t,dirPath:r.dirname(e)}))}validateFilter(e){if("function"!=typeof e)throw new TypeError("filter must be a function")}initNode(){return new o({usePathCache:this._usePathCache})}buildChildren(e,i,n){const o=[],a=i.depth+1,h=n||t.readdirSync(e);for(const t of h){const n=r.join(e,t);if(!this.applyFilter(n,t))continue;const h=s(n),l=this.initNode();l.depth=a,l.value=t,l.parent=i,o.push(l),this._fileVisible&&h===exports.PathTreeNodeKind.File?l.type=h:h===exports.PathTreeNodeKind.Dir?(l.type=h,l.children=this.buildChildren(n,l)):l.type=h}return o}checkRelativePaths(e){for(let t=0;t<e.length;t++){const i=e[t];if("string"!=typeof i)throw new Error(`Item at index ${t} is not a string, got ${typeof i}`);const s=r.resolve(this._base,i);if(!n.isValid(s))throw new Error(`Path does not exist or is not accessible: ${s} (from relative path: ${i})`);if(!(n.isDirectory(s)||this._fileVisible&&n.isFile(s)))throw new Error(`Path is not a directory: ${s} (from relative path: ${i})`)}}formatSegments(e){return e.map(e=>e.split(/[/\\]/).filter(Boolean).join(r.sep)).filter(Boolean)}getAllEntriesUnderBase(){return t.readdirSync(this._base).filter(e=>{const t=r.resolve(this._base,e);return this.applyFilter(t,e)})}buildBySegments(e){const t=this.initNode();return t.depth=0,t.value=this._base,t.type=exports.PathTreeNodeKind.Dir,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,statSync as r,accessSync as e,constants as i}from"fs";import{join as s,resolve as a,sep as n}from"path";class o{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}}}class l{constructor(t){this.parent=null,this.value="",this.children=[],this.base=t}getPath(){let t="",r=this;for(;r.parent;)t=t?`${r.value}${n}${t}`:r.value,r=r.parent;return{relative:t,absolute:a(this.base,t)}}}class h{constructor({filter:t,base:r}){if(void 0!==t&&(this.validateFilter(t),this.filter=t),!r||!o.isValid(r))throw new Error(`${r} is not a valid path!`);if(!o.isDirectory(r))throw new Error(`${r} is not a dirPath!`);this.base=r}validateFilter(t){if("function"!=typeof t)throw new TypeError("filter must be a function");if(1!==t.length)throw new TypeError("filter must accept exactly one parameter");try{if("boolean"!=typeof t({name:"test",postPath:"/test"}))throw new TypeError("filter must return a boolean")}catch(t){throw new TypeError("filter function threw an error during test: "+t)}}initNode(t=null){const r=new l(this.base);return t&&(r.parent=t),r}buildChildren(e,i){const a=t(e),n=[];for(const t of a){const a=s(e,t);if(!r(a).isDirectory())continue;if(this.filter&&!this.filter({dirPath:e,name:t}))continue;const o=this.initNode();o.value=t,o.parent=i,o.children=this.buildChildren(a,o),n.push(o)}return n}checkRelativePaths(t){if(!Array.isArray(t))throw new Error("Expected array, got "+typeof t);for(let r=0;r<t.length;r++){const e=t[r];if("string"!=typeof e)throw new Error(`Item at index ${r} is not a string, got ${typeof e}`);const i=a(this.base,e);if(!o.isValid(i))throw new Error(`Path does not exist or is not accessible: ${i} (from relative path: ${e})`);if(!o.isDirectory(i))throw new Error(`Path is not a directory: ${i} (from relative path: ${e})`)}}formatDirnames(t){return t.map(t=>t.replace(/^\/+|\/+$/g,"")).filter(t=>""!==t)}getAllDirNamesUnderBase(){return t(this.base).filter(t=>{const r=a(this.base,t);return!!o.isDirectory(r)&&!(this.filter&&!this.filter({name:t,dirPath:this.base}))})}buildByDirNames(t){const r=this.initNode(),e=this.formatDirnames(t);this.checkRelativePaths(e);for(const t of e){const e=this.initNode();e.value=t,e.parent=r,e.children=this.buildChildren(a(this.base,t),e),r.children.push(e)}return r}buildByFilter(t){const r=this.getAllDirNamesUnderBase();return this.buildByDirNames(r.filter(t))}getPathBy(t){let r="",e=t;for(;e.parent;)r=r?`${e.value}${n}${r}`:e.value,e=e.parent;return{relative:r,absolute:a(this.base,r)}}buildBy(t){if(Array.isArray(t))return this.buildByDirNames(t);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.getAllDirNamesUnderBase();return this.buildByDirNames(t)}}export{h as PathTreeify};
1
+ import{accessSync as t,constants as e,statSync as i,lstatSync as r,readdirSync as s}from"fs";import{sep as n,resolve as o,dirname as a,join as l}from"path";var h;function c(t,e){for(const i in e)Object.defineProperty(t,i,{value:e[i],writable:!1,configurable:!1,enumerable:!1})}function u(t){try{return function(t){try{const e=i(t);return e.isDirectory()?h.Dir:e.isFile()?h.File:h.Other}catch(t){if("ENOENT"!==t.code)throw t}try{return r(t).isSymbolicLink()?h.BrokenSymlink:h.Other}catch(t){if("ENOENT"!==t.code)throw t}return h.NotFound}(t)}catch(t){return h.Error}}!function(t){t.Dir="dir",t.File="file",t.Unknown="unknown",t.BrokenSymlink="broken_symlink",t.Other="other",t.NotFound="not_found",t.Error="error"}(h||(h={}));class f{static isValid(i){try{return t(i,e.F_OK),!0}catch{return!1}}static isDirectory(t){try{return i(t).isDirectory()}catch{return!1}}static isFile(t){try{return i(t).isFile()}catch{return!1}}}class d{constructor(t){this.depth=-1,this.parent=null,this.value="",this.children=[],this.type=h.Unknown,c(this,{_usePathCache:t.usePathCache})}getPath(){let t=this,e="";const i=()=>{let t="",i=this;for(;;){if(null===i.parent){e=i.value;break}t=t?`${i.value}${n}${t}`:i.value,i=i.parent}return{relative:t,absolute:o(e,t)}};return t._usePathCache?(t._pathCache||c(this,{_pathCache:i()}),t._pathCache):i()}}class y{constructor(t){this._base="",this._fileVisible=!1,this._usePathCache=!1;const{filter:e,base:i,fileVisible:r,usePathCache:s}=t;if(void 0!==e&&(this.validateFilter(e),c(this,{_userFilter:e})),!i||!f.isValid(i))throw new Error(`${i} is not a valid path!`);if(!f.isDirectory(i))throw new Error(`${i} is not a dirPath!`);c(this,{_usePathCache:Boolean(s),_base:i,_fileVisible:!("boolean"!=typeof r||!r)&&r})}applyFilter(t,e){return!(!this._fileVisible&&!f.isDirectory(t))&&(!this._userFilter||this._userFilter({name:e,dirPath:a(t)}))}validateFilter(t){if("function"!=typeof t)throw new TypeError("filter must be a function")}initNode(){return new d({usePathCache:this._usePathCache})}buildChildren(t,e,i){const r=[],n=e.depth+1,o=i||s(t);for(const i of o){const s=l(t,i);if(!this.applyFilter(s,i))continue;const o=u(s),a=this.initNode();a.depth=n,a.value=i,a.parent=e,r.push(a),this._fileVisible&&o===h.File?a.type=o:o===h.Dir?(a.type=o,a.children=this.buildChildren(s,a)):a.type=o}return r}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=o(this._base,i);if(!f.isValid(r))throw new Error(`Path does not exist or is not accessible: ${r} (from relative path: ${i})`);if(!(f.isDirectory(r)||this._fileVisible&&f.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(n)).filter(Boolean)}getAllEntriesUnderBase(){return s(this._base).filter(t=>{const e=o(this._base,t);return this.applyFilter(e,t)})}buildBySegments(t){const e=this.initNode();return e.depth=0,e.value=this._base,e.type=h.Dir,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{h as PathTreeNodeKind,y as PathTreeify};
@@ -1,93 +1,93 @@
1
- /** A filter function that determines whether a directory should be included in the tree */
2
- type FilterFunction = (params: {
3
- name: string;
4
- dirPath: string;
5
- }) => boolean;
6
- /** Constructor options for PathTreeify */
7
- interface PathTreeifyProps {
8
- base: string;
9
- filter?: FilterFunction;
10
- }
11
- /** Represents a single node (directory) in the path tree */
12
- declare class PathTreeNode {
13
- /** The root base path used to resolve absolute paths */
14
- private base;
15
- /** Reference to the parent node; null for the root node */
16
- parent: PathTreeNode | null;
17
- /** The directory name of this node (not a full path) */
18
- value: string;
19
- /** Child nodes representing subdirectories */
20
- children: PathTreeNode[];
21
- constructor(base: string);
22
- /**
23
- * Walks up the parent chain to compute this node's relative and absolute paths.
24
- * @returns An object containing the relative path from base and the absolute path
25
- */
26
- getPath(): {
27
- relative: string;
28
- absolute: string;
29
- };
30
- }
31
- /** Builds a tree of directory nodes rooted at a given base path */
1
+ import { PathTreeifyProps, PathTreeNode } from './src/types';
2
+ export { PathTreeifyProps, PathTreeNodeKind, PathTreeNode } from './src/types';
3
+ /** Builds a tree of {@link PathTreeNode} entries rooted at a given base path */
32
4
  export declare class PathTreeify {
33
5
  /** The root directory to scan */
34
- private base;
35
- /** Optional filter applied to each directory during traversal */
36
- private filter?;
37
- constructor({ filter, base }: Partial<PathTreeifyProps>);
6
+ private _base;
7
+ /** When true, files are included as leaf nodes during traversal. Defaults to false */
8
+ private _fileVisible;
9
+ /** When true, absolute paths are cached on each node after the first retrieval to speed up subsequent `getPath` calls at the cost of higher memory usage. Defaults to false */
10
+ private _usePathCache;
11
+ /**
12
+ * Optional user-supplied filter. When set, every entry must pass this predicate
13
+ * in addition to the built-in visibility check.
14
+ */
15
+ private _userFilter?;
16
+ constructor(props: Partial<PathTreeifyProps>);
17
+ /**
18
+ * Determines whether a given entry should be included in the tree.
19
+ * - If {@link fileVisible} is false, non-directory entries are always excluded.
20
+ * - If a {@link userFilter} is set, the entry must also satisfy it.
21
+ * @param absPath - Absolute path of the entry to test
22
+ * @param name - Entry name (filename or directory name)
23
+ */
24
+ private applyFilter;
38
25
  /**
39
- * Validates that the provided filter is a function, accepts one parameter,
40
- * and returns a boolean. Throws a TypeError if any condition is violated.
26
+ * Asserts that the provided value is a callable {@link FilterFunction}.
27
+ * Throws a TypeError if the check fails.
41
28
  */
42
29
  private validateFilter;
43
30
  /**
44
- * Creates and optionally attaches a new PathTreeNode to a parent.
45
- * @param parent - The parent node to attach to, or null for the root
31
+ * Creates a new unattached {@link PathTreeNode}.
32
+ * The node is created with a two-layer prototype chain:
33
+ * `node β†’ cache β†’ pathTreeNodeShared`. The intermediate `cache` layer is a
34
+ * per-node object that holds `_pathCache` when `usePathCache` is enabled,
35
+ * keeping the cached value isolated to each node while still inheriting `base`
36
+ * and `getPath` from `pathTreeNodeShared`.
37
+ * `depth` is initialised to `-1` and must be set by the caller.
46
38
  */
47
39
  private initNode;
48
40
  /**
49
- * Recursively reads a directory and builds child nodes for each subdirectory.
50
- * Applies the instance-level filter if one is set.
51
- * @param dirPath - Absolute path of the directory to read
52
- * @param parent - The parent node to attach children to
41
+ * Recursively reads {@link dirPath} and builds child nodes for each entry that
42
+ * passes {@link applyFilter}. Directories are traversed depth-first;
43
+ * files (when {@link fileVisible} is true) become leaf nodes.
44
+ *
45
+ * @param dirPath - Absolute path of the directory to read
46
+ * @param parent - The parent node to attach child nodes to
47
+ * @param segments - Optional explicit list of entry names to use instead of reading the
48
+ * directory from disk; used by {@link buildBySegments} to skip a
49
+ * redundant `readdirSync` when the segment list is already known
53
50
  */
54
51
  private buildChildren;
55
52
  /**
56
- * Validates that each entry in the array is a string pointing to
57
- * an accessible directory relative to the base path.
58
- * @param relativeDirNames - Array of relative directory path strings to validate
53
+ * Validates that every entry in {@link relativeSegments} refers to an accessible
54
+ * path under {@link base}. When {@link fileVisible} is false, each path must be
55
+ * a directory; when true, regular files are also accepted.
56
+ * @param relativeSegments - Relative path strings to validate; assumed to be a string
57
+ * array (callers are responsible for type safety at the boundary)
59
58
  */
60
59
  private checkRelativePaths;
61
60
  /**
62
- * Strips leading and trailing slashes from each directory name
63
- * and removes any resulting empty strings.
61
+ * Normalises an array of path strings by splitting on both slash styles,
62
+ * dropping empty segments, and rejoining with the platform separator.
63
+ * Entries that reduce to an empty string (e.g. `"///"`) are removed.
64
+ * @param segments - Raw path strings to normalise
64
65
  */
65
- private formatDirnames;
66
- /** Returns the names of all immediate subdirectories under the base path */
67
- private getAllDirNamesUnderBase;
66
+ private formatSegments;
68
67
  /**
69
- * Builds a tree rooted at base, containing only the specified subdirectories.
70
- * @param dirNames - Relative directory names to include as top-level nodes
68
+ * Returns the names of all immediate entries under {@link base} that pass
69
+ * {@link applyFilter}.
71
70
  */
72
- private buildByDirNames;
71
+ private getAllEntriesUnderBase;
73
72
  /**
74
- * Builds a tree using only the subdirectories under base that pass the given filter.
75
- * @param filter - A predicate applied to each top-level directory name
73
+ * Builds a subtree whose top-depth children correspond to {@link segments}.
74
+ * The root node is created at depth 0; children are built by delegating to
75
+ * {@link buildChildren}, passing {@link segments} directly to avoid a redundant
76
+ * `readdirSync` of the base directory.
77
+ * @param segments - Normalised and validated relative path segments
76
78
  */
77
- private buildByFilter;
79
+ private buildBySegments;
78
80
  /**
79
- * Computes the relative and absolute paths for a given node
80
- * by walking up the parent chain.
81
+ * Builds a subtree from top-depth entries whose names satisfy {@link filter}.
82
+ * Note: this predicate only affects top-depth selection, not recursive traversal.
83
+ * For recursive filtering use the `filter` constructor option.
84
+ * @param filter - Predicate applied to each top-depth entry name
81
85
  */
82
- getPathBy(node: PathTreeNode): {
83
- relative: string;
84
- absolute: string;
85
- };
86
- /** Overload: build the tree from an explicit list of relative directory names */
87
- buildBy(dirNames: string[]): PathTreeNode;
88
- /** Overload: build the tree from a predicate applied to top-level directory names */
89
- buildBy(filter: (dirName: string) => boolean): PathTreeNode;
90
- /** Builds a full tree from all immediate subdirectories under the base path */
86
+ private buildByFilter;
87
+ /** Overload: build the tree from an explicit list of relative path segments */
88
+ buildBy(segments: string[]): PathTreeNode;
89
+ /** Overload: build the tree from a predicate applied to top-depth entry names */
90
+ buildBy(filter: (segment: string) => boolean): PathTreeNode;
91
+ /** Builds a full tree from all immediate entries under the base path */
91
92
  build(): PathTreeNode;
92
93
  }
93
- export {};
@@ -0,0 +1,59 @@
1
+ /** A filter function that determines whether an entry should be included in the tree */
2
+ export type FilterFunction = (params: {
3
+ name: string;
4
+ dirPath: string;
5
+ }) => boolean;
6
+ /** Constructor options for PathTreeify */
7
+ export interface PathTreeifyProps {
8
+ /** The root directory to scan */
9
+ base: string;
10
+ /** Optional filter applied to every entry during recursive traversal */
11
+ filter?: FilterFunction;
12
+ /** When true, files are included as leaf nodes alongside directories. Defaults to false */
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;
21
+ }
22
+ /** Classification of a node in the path tree */
23
+ export declare enum PathTreeNodeKind {
24
+ Dir = "dir",
25
+ File = "file",
26
+ /** Assigned before the node's type has been resolved */
27
+ Unknown = "unknown",
28
+ BrokenSymlink = "broken_symlink",
29
+ Other = "other",
30
+ NotFound = "not_found",
31
+ Error = "error"
32
+ }
33
+ /**
34
+ * Public interface for a node in the path tree.
35
+ * Consumers receive this type; the internal implementation class is not exported.
36
+ */
37
+ export interface PathTreeNode {
38
+ /** Distance from the root node; root itself is 0, its direct children are 1, and so on */
39
+ depth: number;
40
+ /** Reference to the parent node; null for the root node */
41
+ parent: PathTreeNode | null;
42
+ /** The entry name of this node (not a full path) */
43
+ value: string;
44
+ /** Child nodes; non-empty only for directory nodes */
45
+ children: PathTreeNode[];
46
+ /** Whether this node is a directory, a file, or not yet resolved */
47
+ type: PathTreeNodeKind;
48
+ /**
49
+ * Walks up the parent chain to compute this node's relative and absolute paths.
50
+ * When the owning {@link PathTreeify} instance was created with `usePathCache: true`,
51
+ * the result is memoised after the first call and the same object is returned on
52
+ * every subsequent call.
53
+ * @returns `relative` β€” path from the tree root; `absolute` β€” fully resolved path on disk
54
+ */
55
+ getPath(): {
56
+ relative: string;
57
+ absolute: string;
58
+ };
59
+ }
@@ -0,0 +1,38 @@
1
+ import { PathTreeNode, PathTreeNodeKind } from "./types";
2
+ /** Defines read-only properties on an object. Each property is set to the corresponding value in the props object, and is non-writable, non-configurable, and non-enumerable.
3
+ * @param obj - The target object on which to define the properties
4
+ * @param props - An object where keys are property names and values are the corresponding property values to set
5
+ */
6
+ export declare function defineReadOnlyProps(obj: any, props: {
7
+ [key: string]: any;
8
+ }): void;
9
+ /** Determines the type of a given path, classifying it as a directory, file, symbolic link, or other. If the path does not exist, it is classified as NotFound. If any unexpected error occurs during the stat/lstat operations, it will be thrown to the caller.
10
+ * @param p - The path to classify
11
+ * @returns A PathTreeNodeKind value indicating the type of the path
12
+ */
13
+ export declare function getPathType(p: string): PathTreeNodeKind;
14
+ export declare function getSafePathType(p: string): PathTreeNodeKind;
15
+ /** Utility class for validating file system paths */
16
+ export declare class PathValidator {
17
+ /** Returns true if the path exists and is accessible */
18
+ static isValid(path: string): boolean;
19
+ /** Returns true if the path points to a directory */
20
+ static isDirectory(path: string): boolean;
21
+ /** Returns true if the path points to a regular file */
22
+ static isFile(path: string): boolean;
23
+ }
24
+ export declare class PathTreeNodeImp implements PathTreeNode {
25
+ depth: number;
26
+ parent: PathTreeNode | null;
27
+ value: string;
28
+ children: PathTreeNode[];
29
+ type: PathTreeNodeKind;
30
+ constructor(props: {
31
+ usePathCache: boolean;
32
+ });
33
+ /** Retrieves the absolute and relative path represented by this node. If path caching is enabled, the result will be cached after the first retrieval to optimize subsequent calls. */
34
+ getPath(): {
35
+ relative: string;
36
+ absolute: string;
37
+ };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-treeify",
3
- "version": "1.3.0",
3
+ "version": "1.4.0-beta.31d8864",
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",
@@ -26,13 +26,13 @@
26
26
  "scripts": {
27
27
  "test": "ava",
28
28
  "test:coverage": "c8 ava",
29
- "test:junit": "mkdir -p reports && ava --tap | tap-xunit > reports/junit.xml",
30
- "test:report": "rimraf reports && npm run test:coverage && npm run test:junit",
29
+ "test:md": "mkdir -p reports && ava --tap | tap-json | node ./.github/scripts/generate-report.js --title \"${npm_config_title:-AVA Test Results}\" > reports/ava-test.md",
30
+ "test:report": "rimraf reports && npm run test:coverage && npm run test:md",
31
31
  "clean": "rimraf dist",
32
32
  "build": "npm run clean && rollup -c",
33
33
  "build:dev": "NODE_ENV=development npm run build",
34
34
  "build:prod": "NODE_ENV=production npm run build",
35
- "build:watch": "nodemon --watch index.ts -e ts --exec \"npm run build:dev\"",
35
+ "dev": "nodemon --watch index.ts -e ts --exec \"npm run build:dev\"",
36
36
  "check-publish": "npm pack --dry-run",
37
37
  "publish:beta": "npm publish --tag beta --access public",
38
38
  "publish:latest": "npm publish --tag latest --access public"
@@ -93,8 +93,9 @@
93
93
  "nodemon": "^3.1.14",
94
94
  "rimraf": "^6.1.3",
95
95
  "rollup": "^4.59.0",
96
- "tap-xunit": "^2.4.1",
96
+ "tap-json": "^1.0.0",
97
97
  "tslib": "^2.8.1",
98
+ "tsx": "^4.21.0",
98
99
  "typescript": "^5.9.3"
99
100
  },
100
101
  "engines": {