path-treeify 1.3.0-beta.45ab046 β†’ 1.3.0-beta.6923d77

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>
@@ -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,11 @@
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
+ - πŸ‘οΈ `fileVisible` option includes files as leaf nodes alongside directories
55
+ - πŸ” Optional `filter` callback applied at **every depth**, including top-level entries
48
56
  - ⚑ `build()` scans the entire `base` directory with zero configuration
49
- - πŸŽ›οΈ `buildBy()` accepts either a directory name array or a top-level filter function
57
+ - πŸŽ›οΈ `buildBy()` accepts either a path segment array or a top-level filter function
50
58
  - πŸ“¦ Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
51
59
  - 🚫 Zero runtime dependencies
52
60
 
@@ -73,18 +81,27 @@ yarn add path-treeify
73
81
  ## Quick Start
74
82
 
75
83
  ```ts
76
- import { PathTreeify } from 'path-treeify';
84
+ import { PathTreeify, PathTreeNodeKind } from 'path-treeify';
77
85
 
86
+ // Directories only (default)
78
87
  const treeify = new PathTreeify({ base: '/your/project/root' });
88
+ const tree = treeify.build();
79
89
 
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();
90
+ // Include files as leaf nodes
91
+ const treeifyWithFiles = new PathTreeify({
92
+ base: '/your/project/root',
93
+ fileVisible: true,
94
+ });
95
+ const fullTree = treeifyWithFiles.build();
96
+
97
+ // Check node type
98
+ for (const child of tree.children) {
99
+ if (child.type === PathTreeNodeKind.Dir) {
100
+ console.log('dir:', child.value);
101
+ } else if (child.type === PathTreeNodeKind.File) {
102
+ console.log('file:', child.value);
103
+ }
104
+ }
88
105
  ```
89
106
 
90
107
  ---
@@ -95,10 +112,11 @@ const fullTree = treeify.build();
95
112
 
96
113
  Creates a new instance.
97
114
 
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 |
115
+ | Option | Type | Required | Description |
116
+ |---------------|------------------------------|----------|--------------------------------------------------------------------------|
117
+ | `base` | `string` | βœ… | Absolute path to the root directory to scan from |
118
+ | `filter` | `FilterFunction` (see below) | ❌ | Applied at **every depth** β€” top-level entries included |
119
+ | `fileVisible` | `boolean` | ❌ | When `true`, files are included as leaf nodes. Defaults to `false` |
102
120
 
103
121
  `base` must exist and be a directory, otherwise the constructor throws.
104
122
 
@@ -110,14 +128,14 @@ Used as the `filter` option in the constructor. Applied at every level of the tr
110
128
 
111
129
  ```ts
112
130
  type FilterFunction = (params: {
113
- name: string; // directory name (leaf segment)
131
+ name: string; // entry name (file or directory name)
114
132
  dirPath: string; // absolute path of the parent directory
115
133
  }) => boolean;
116
134
  ```
117
135
 
118
- Return `true` to **include** the directory and recurse into it; `false` to **skip** it entirely.
136
+ Return `true` to **include** the entry; `false` to **skip** it entirely.
119
137
 
120
- **Example β€” exclude `node_modules` and hidden directories at every depth:**
138
+ **Example β€” exclude `node_modules` and hidden entries at every depth:**
121
139
 
122
140
  ```ts
123
141
  const treeify = new PathTreeify({
@@ -130,7 +148,7 @@ const treeify = new PathTreeify({
130
148
 
131
149
  ### `build(): PathTreeNode`
132
150
 
133
- Scans all subdirectories directly under `base` (applying the instance-level `filter` if set) and returns a synthetic root `PathTreeNode`.
151
+ 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
152
 
135
153
  ```ts
136
154
  const tree = treeify.build();
@@ -138,64 +156,70 @@ const tree = treeify.build();
138
156
 
139
157
  ---
140
158
 
141
- ### `buildBy(dirNames: string[]): PathTreeNode`
159
+ ### `buildBy(segments: string[]): PathTreeNode`
142
160
 
143
- Builds a tree from the given list of directory names (relative to `base`).
161
+ Builds a tree from the given list of relative path segments. When `fileVisible` is `true`, file paths are also accepted.
144
162
 
145
163
  ```ts
146
164
  const tree = treeify.buildBy(['src', 'docs', 'tests']);
165
+
166
+ // With fileVisible: true, files can also be specified
167
+ const treeWithFiles = new PathTreeify({ base: '/your/project', fileVisible: true });
168
+ treeWithFiles.buildBy(['src', 'README.md']);
147
169
  ```
148
170
 
149
- - Leading and trailing slashes are stripped automatically.
150
- - Throws if any name does not resolve to a valid directory under `base`.
171
+ - Both `/` and `\` separators are normalised automatically.
172
+ - Leading/trailing slashes and empty segments are stripped.
173
+ - Throws if any segment does not resolve to a valid entry under `base`.
151
174
 
152
- ### `buildBy(filter: (dirName: string) => boolean): PathTreeNode`
175
+ ### `buildBy(filter: (segment: string) => boolean): PathTreeNode`
153
176
 
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.
177
+ 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
178
 
156
179
  ```ts
157
180
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
158
181
  ```
159
182
 
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.
183
+ > **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
184
 
162
185
  ---
163
186
 
164
- ### `getPathBy(node: PathTreeNode): { relative: string; absolute: string }`
187
+ ### `PathTreeNode`
165
188
 
166
- Walks a node's `parent` chain to reconstruct its full path. Equivalent to calling `node.getPath()` directly.
189
+ `PathTreeNode` is an **interface** β€” each node exposes a `getPath()` method to retrieve its paths without needing the `PathTreeify` instance.
167
190
 
168
191
  ```ts
169
- const { relative, absolute } = treeify.getPathBy(node);
170
- // relative β†’ e.g. 'src/components'
171
- // absolute β†’ e.g. '/your/project/src/components'
192
+ interface PathTreeNode {
193
+ parent: PathTreeNode | null; // null only on the synthetic root
194
+ value: string; // entry name for this node
195
+ children: PathTreeNode[]; // empty for file nodes
196
+ type: PathTreeNodeKind; // Dir, File, or Unknown
197
+
198
+ getPath(): { relative: string; absolute: string };
199
+ }
172
200
  ```
173
201
 
202
+ > ⚠️ **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
+
174
204
  ---
175
205
 
176
- ### `PathTreeNode`
206
+ ### `PathTreeNodeKind`
177
207
 
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.
208
+ An enum classifying each node's filesystem type.
179
209
 
180
210
  ```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 };
211
+ enum PathTreeNodeKind {
212
+ Dir = 'dir',
213
+ File = 'file',
214
+ Unknown = 'unknown', // assigned before the type is resolved
187
215
  }
188
216
  ```
189
217
 
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
218
  ---
195
219
 
196
220
  ## Examples
197
221
 
198
- ### Scan an entire base directory
222
+ ### Directories only (default)
199
223
 
200
224
  ```ts
201
225
  import { PathTreeify } from 'path-treeify';
@@ -204,6 +228,16 @@ const treeify = new PathTreeify({ base: '/your/project' });
204
228
  const tree = treeify.build();
205
229
  ```
206
230
 
231
+ ### Include files as leaf nodes
232
+
233
+ ```ts
234
+ const treeify = new PathTreeify({
235
+ base: '/your/project',
236
+ fileVisible: true,
237
+ });
238
+ const tree = treeify.build();
239
+ ```
240
+
207
241
  ### Exclude directories at every depth via constructor filter
208
242
 
209
243
  ```ts
@@ -214,13 +248,13 @@ const treeify = new PathTreeify({
214
248
  const tree = treeify.build();
215
249
  ```
216
250
 
217
- ### Scan specific directories
251
+ ### Scan specific paths
218
252
 
219
253
  ```ts
220
254
  const tree = treeify.buildBy(['src', 'tests', 'docs']);
221
255
  ```
222
256
 
223
- ### Select top-level directories with a predicate
257
+ ### Select top-level entries with a predicate
224
258
 
225
259
  ```ts
226
260
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
@@ -232,7 +266,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
232
266
  function printPaths(node) {
233
267
  for (const child of node.children) {
234
268
  const { absolute } = child.getPath();
235
- console.log(absolute);
269
+ console.log(`[${child.type}] ${absolute}`);
236
270
  printPaths(child);
237
271
  }
238
272
  }
@@ -243,7 +277,7 @@ printPaths(tree);
243
277
  ### CommonJS usage
244
278
 
245
279
  ```js
246
- const { PathTreeify } = require('path-treeify');
280
+ const { PathTreeify, PathTreeNodeKind } = require('path-treeify');
247
281
 
248
282
  const treeify = new PathTreeify({ base: __dirname });
249
283
  const tree = treeify.build();
@@ -253,4 +287,4 @@ const tree = treeify.build();
253
287
 
254
288
  ## License
255
289
 
256
- [MIT](https://github.com/isaaxite/path-treeify/blob/main/LICENSE) Β© [isaaxite](https://github.com/isaaxite)
290
+ [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"),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,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{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 {};
@@ -1,93 +1,144 @@
1
- /** A filter function that determines whether a directory should be included in the tree */
1
+ /** A filter function that determines whether an entry should be included in the tree */
2
2
  type FilterFunction = (params: {
3
3
  name: string;
4
4
  dirPath: string;
5
5
  }) => boolean;
6
6
  /** Constructor options for PathTreeify */
7
7
  interface PathTreeifyProps {
8
+ /** The root directory to scan */
8
9
  base: string;
10
+ /** Optional filter applied to every entry during recursive traversal */
9
11
  filter?: FilterFunction;
12
+ /** When true, files are included as leaf nodes alongside directories. Defaults to false */
13
+ fileVisible?: boolean;
14
+ usePathCache?: boolean;
10
15
  }
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;
16
+ /** Classification of a node in the path tree */
17
+ export declare enum PathTreeNodeKind {
18
+ Dir = "dir",
19
+ File = "file",
20
+ /** Assigned before the node's type has been resolved */
21
+ Unknown = "unknown"
22
+ }
23
+ /**
24
+ * Public interface for a node in the path tree.
25
+ * Consumers receive this type; the internal implementation class is not exported.
26
+ */
27
+ export interface PathTreeNode {
28
+ /** Distance from the root node; root itself is 0, its direct children are 1, and so on */
29
+ depth: number;
15
30
  /** Reference to the parent node; null for the root node */
16
31
  parent: PathTreeNode | null;
17
- /** The directory name of this node (not a full path) */
32
+ /** The entry name of this node (not a full path) */
18
33
  value: string;
19
- /** Child nodes representing subdirectories */
34
+ /** Child nodes; non-empty only for directory nodes */
20
35
  children: PathTreeNode[];
21
- constructor(base: string);
36
+ /** Whether this node is a directory, a file, or not yet resolved */
37
+ type: PathTreeNodeKind;
22
38
  /**
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
39
+ * Computes this node's paths using the `parentRelative` stored on the siblings'
40
+ * shared prototype by {@link PathTreeify.buildChildren}.
41
+ * @returns `relative` β€” path from the tree root; `absolute` β€” fully resolved path on disk
25
42
  */
26
43
  getPath(): {
27
44
  relative: string;
28
45
  absolute: string;
29
46
  };
30
47
  }
31
- /** Builds a tree of directory nodes rooted at a given base path */
48
+ /** Builds a tree of {@link PathTreeNode} entries rooted at a given base path */
32
49
  export declare class PathTreeify {
33
50
  /** The root directory to scan */
34
51
  private base;
35
- /** Optional filter applied to each directory during traversal */
36
- private filter?;
37
- constructor({ filter, base }: Partial<PathTreeifyProps>);
38
52
  /**
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.
53
+ * Shared prototype instance for nodes produced by this builder.
54
+ * All nodes created via {@link initNode} inherit `base` and `getPath` from this object,
55
+ * avoiding per-node storage of the base path string.
56
+ */
57
+ private pathTreeNodeShared;
58
+ /**
59
+ * Optional user-supplied filter. When set, every entry must pass this predicate
60
+ * in addition to the built-in visibility check.
61
+ */
62
+ private userFilter?;
63
+ /** When true, files are included as leaf nodes during traversal. Defaults to false */
64
+ private fileVisible;
65
+ constructor(props: Partial<PathTreeifyProps>);
66
+ /**
67
+ * Determines whether a given entry should be included in the tree.
68
+ * - If {@link fileVisible} is false, non-directory entries are always excluded.
69
+ * - If a {@link userFilter} is set, the entry must also satisfy it.
70
+ * @param absPath - Absolute path of the entry to test
71
+ * @param name - Entry name (filename or directory name)
72
+ */
73
+ private applyFilter;
74
+ /**
75
+ * Asserts that the provided value is a callable {@link FilterFunction}.
76
+ * Throws a TypeError if the check fails.
41
77
  */
42
78
  private validateFilter;
43
79
  /**
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
80
+ * Creates a new unattached {@link PathTreeNode}.
81
+ * The node's prototype is set to {@link pathTreeNodeShared} so that `base` and
82
+ * `getPath` are inherited without being stored on each instance individually.
83
+ * `depth` are initialised to `-1` and must be set by the caller.
46
84
  */
47
85
  private initNode;
48
86
  /**
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
87
+ * Recursively reads {@link dirPath} and builds child nodes for each entry that
88
+ * passes {@link applyFilter}. Directories are traversed depth-first;
89
+ * files (when {@link fileVisible} is true) become leaf nodes.
90
+ *
91
+ * After assembling the sibling group, injects `parentRelative` onto a new intermediate
92
+ * prototype shared by `children[0]`. This allows {@link PathTreeNodeShared.getPath} to
93
+ * resolve paths in O(1) without walking the full parent chain on every call.
94
+ *
95
+ * @param dirPath - Absolute path of the directory to read
96
+ * @param parent - The parent node to attach child nodes to
97
+ * @param segments - Optional explicit list of entry names to use instead of reading the
98
+ * directory from disk; used by {@link buildBySegments} to skip a
99
+ * redundant `readdirSync` when the segment list is already known
53
100
  */
54
101
  private buildChildren;
55
102
  /**
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
103
+ * Validates that every entry in {@link relativeSegments} refers to an accessible
104
+ * path under {@link base}. When {@link fileVisible} is false, each path must be
105
+ * a directory; when true, regular files are also accepted.
106
+ * @param relativeSegments - Relative path strings to validate; assumed to be a string
107
+ * array (callers are responsible for type safety at the boundary)
59
108
  */
60
109
  private checkRelativePaths;
61
110
  /**
62
- * Strips leading and trailing slashes from each directory name
63
- * and removes any resulting empty strings.
111
+ * Normalises an array of path strings by splitting on both slash styles,
112
+ * dropping empty segments, and rejoining with the platform separator.
113
+ * Entries that reduce to an empty string (e.g. `"///"`) are removed.
114
+ * @param segments - Raw path strings to normalise
64
115
  */
65
- private formatDirnames;
66
- /** Returns the names of all immediate subdirectories under the base path */
67
- private getAllDirNamesUnderBase;
116
+ private formatSegments;
68
117
  /**
69
- * Builds a tree rooted at base, containing only the specified subdirectories.
70
- * @param dirNames - Relative directory names to include as top-level nodes
118
+ * Returns the names of all immediate entries under {@link base} that pass
119
+ * {@link applyFilter}.
71
120
  */
72
- private buildByDirNames;
121
+ private getAllEntriesUnderBase;
73
122
  /**
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
123
+ * Builds a subtree whose top-depth children correspond to {@link segments}.
124
+ * The root node is created at depth 0; children are built by delegating to
125
+ * {@link buildChildren}, passing {@link segments} directly to avoid a redundant
126
+ * `readdirSync` of the base directory.
127
+ * @param segments - Normalised and validated relative path segments
76
128
  */
77
- private buildByFilter;
129
+ private buildBySegments;
78
130
  /**
79
- * Computes the relative and absolute paths for a given node
80
- * by walking up the parent chain.
131
+ * Builds a subtree from top-depth entries whose names satisfy {@link filter}.
132
+ * Note: this predicate only affects top-depth selection, not recursive traversal.
133
+ * For recursive filtering use the `filter` constructor option.
134
+ * @param filter - Predicate applied to each top-depth entry name
81
135
  */
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 */
136
+ private buildByFilter;
137
+ /** Overload: build the tree from an explicit list of relative path segments */
138
+ buildBy(segments: string[]): PathTreeNode;
139
+ /** Overload: build the tree from a predicate applied to top-depth entry names */
140
+ buildBy(filter: (segment: string) => boolean): PathTreeNode;
141
+ /** Builds a full tree from all immediate entries under the base path */
91
142
  build(): PathTreeNode;
92
143
  }
93
144
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-treeify",
3
- "version": "1.3.0-beta.45ab046",
3
+ "version": "1.3.0-beta.6923d77",
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",
@@ -95,6 +95,7 @@
95
95
  "rollup": "^4.59.0",
96
96
  "tap-xunit": "^2.4.1",
97
97
  "tslib": "^2.8.1",
98
+ "tsx": "^4.21.0",
98
99
  "typescript": "^5.9.3"
99
100
  },
100
101
  "engines": {