path-treeify 1.3.0-beta.56a60fd → 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
@@ -35,8 +35,8 @@
35
35
  <a href="https://github.com/isaaxite/path-treeify/commits/main/">
36
36
  <img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/isaaxite/path-treeify">
37
37
  </a>
38
- <a href='https://codecov.io/github/isaaxite/path-treeify/tests'>
39
- <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' />
40
40
  </a>
41
41
  <a href='https://coveralls.io/github/isaaxite/path-treeify'>
42
42
  <img src='https://coveralls.io/repos/github/isaaxite/path-treeify/badge.svg' alt='Coverage Status' />
@@ -50,9 +50,11 @@
50
50
  - 🌲 Builds a recursive tree from one or more directory paths
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
- - 🔍 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
54
56
  - ⚡ `build()` scans the entire `base` directory with zero configuration
55
- - 🎛️ `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
56
58
  - 📦 Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
57
59
  - 🚫 Zero runtime dependencies
58
60
 
@@ -79,18 +81,27 @@ yarn add path-treeify
79
81
  ## Quick Start
80
82
 
81
83
  ```ts
82
- import { PathTreeify } from 'path-treeify';
84
+ import { PathTreeify, PathTreeNodeKind } from 'path-treeify';
83
85
 
86
+ // Directories only (default)
84
87
  const treeify = new PathTreeify({ base: '/your/project/root' });
88
+ const tree = treeify.build();
85
89
 
86
- // Scan specific directories by name
87
- const tree = treeify.buildBy(['src', 'tests']);
88
-
89
- // Scan with a top-level filter function
90
- const filtered = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
91
-
92
- // Or scan everything under base at once
93
- 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
+ }
94
105
  ```
95
106
 
96
107
  ---
@@ -101,10 +112,11 @@ const fullTree = treeify.build();
101
112
 
102
113
  Creates a new instance.
103
114
 
104
- | Option | Type | Required | Description |
105
- |----------|-------------------------------|----------|-----------------------------------------------------------------------------|
106
- | `base` | `string` | ✅ | Absolute path to the root directory to scan from |
107
- | `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` |
108
120
 
109
121
  `base` must exist and be a directory, otherwise the constructor throws.
110
122
 
@@ -116,14 +128,14 @@ Used as the `filter` option in the constructor. Applied at every level of the tr
116
128
 
117
129
  ```ts
118
130
  type FilterFunction = (params: {
119
- name: string; // directory name (leaf segment)
131
+ name: string; // entry name (file or directory name)
120
132
  dirPath: string; // absolute path of the parent directory
121
133
  }) => boolean;
122
134
  ```
123
135
 
124
- 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.
125
137
 
126
- **Example — exclude `node_modules` and hidden directories at every depth:**
138
+ **Example — exclude `node_modules` and hidden entries at every depth:**
127
139
 
128
140
  ```ts
129
141
  const treeify = new PathTreeify({
@@ -136,7 +148,7 @@ const treeify = new PathTreeify({
136
148
 
137
149
  ### `build(): PathTreeNode`
138
150
 
139
- 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.
140
152
 
141
153
  ```ts
142
154
  const tree = treeify.build();
@@ -144,64 +156,70 @@ const tree = treeify.build();
144
156
 
145
157
  ---
146
158
 
147
- ### `buildBy(dirNames: string[]): PathTreeNode`
159
+ ### `buildBy(segments: string[]): PathTreeNode`
148
160
 
149
- 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.
150
162
 
151
163
  ```ts
152
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']);
153
169
  ```
154
170
 
155
- - Leading and trailing slashes are stripped automatically.
156
- - 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`.
157
174
 
158
- ### `buildBy(filter: (dirName: string) => boolean): PathTreeNode`
175
+ ### `buildBy(filter: (segment: string) => boolean): PathTreeNode`
159
176
 
160
- 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.
161
178
 
162
179
  ```ts
163
180
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
164
181
  ```
165
182
 
166
- > **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.
167
184
 
168
185
  ---
169
186
 
170
- ### `getPathBy(node: PathTreeNode): { relative: string; absolute: string }`
187
+ ### `PathTreeNode`
171
188
 
172
- 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.
173
190
 
174
191
  ```ts
175
- const { relative, absolute } = treeify.getPathBy(node);
176
- // relative e.g. 'src/components'
177
- // 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
+ }
178
200
  ```
179
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
+
180
204
  ---
181
205
 
182
- ### `PathTreeNode`
206
+ ### `PathTreeNodeKind`
183
207
 
184
- `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.
185
209
 
186
210
  ```ts
187
- class PathTreeNode {
188
- parent: PathTreeNode | null; // null only on the synthetic root
189
- value: string; // directory name for this node
190
- children: PathTreeNode[];
191
-
192
- getPath(): { relative: string; absolute: string };
211
+ enum PathTreeNodeKind {
212
+ Dir = 'dir',
213
+ File = 'file',
214
+ Unknown = 'unknown', // assigned before the type is resolved
193
215
  }
194
216
  ```
195
217
 
196
- `node.getPath()` returns the same result as `treeify.getPathBy(node)` — both are available for convenience.
197
-
198
- > ⚠️ **Circular references** — `parent` points back up the tree. Use `JSON.stringify` replacers or a library like `flatted` if you need to serialize the result.
199
-
200
218
  ---
201
219
 
202
220
  ## Examples
203
221
 
204
- ### Scan an entire base directory
222
+ ### Directories only (default)
205
223
 
206
224
  ```ts
207
225
  import { PathTreeify } from 'path-treeify';
@@ -210,6 +228,16 @@ const treeify = new PathTreeify({ base: '/your/project' });
210
228
  const tree = treeify.build();
211
229
  ```
212
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
+
213
241
  ### Exclude directories at every depth via constructor filter
214
242
 
215
243
  ```ts
@@ -220,13 +248,13 @@ const treeify = new PathTreeify({
220
248
  const tree = treeify.build();
221
249
  ```
222
250
 
223
- ### Scan specific directories
251
+ ### Scan specific paths
224
252
 
225
253
  ```ts
226
254
  const tree = treeify.buildBy(['src', 'tests', 'docs']);
227
255
  ```
228
256
 
229
- ### Select top-level directories with a predicate
257
+ ### Select top-level entries with a predicate
230
258
 
231
259
  ```ts
232
260
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
@@ -238,7 +266,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
238
266
  function printPaths(node) {
239
267
  for (const child of node.children) {
240
268
  const { absolute } = child.getPath();
241
- console.log(absolute);
269
+ console.log(`[${child.type}] ${absolute}`);
242
270
  printPaths(child);
243
271
  }
244
272
  }
@@ -249,7 +277,7 @@ printPaths(tree);
249
277
  ### CommonJS usage
250
278
 
251
279
  ```js
252
- const { PathTreeify } = require('path-treeify');
280
+ const { PathTreeify, PathTreeNodeKind } = require('path-treeify');
253
281
 
254
282
  const treeify = new PathTreeify({ base: __dirname });
255
283
  const tree = treeify.build();
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var e,t=require("fs"),r=require("path");class i{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.PathTreeNodeType=void 0,(e=exports.PathTreeNodeType||(exports.PathTreeNodeType={})).Dir="dir",e.File="file",e.Unknown="unknown";class s{constructor(){this.parent=null,this.value="",this.children=[],this.type=exports.PathTreeNodeType.Unknown}getPath(){let e="",t=this;for(;t.parent;)e=e?`${t.value}${r.sep}${e}`:t.value,t=t.parent;return e}}exports.PathTreeNode=s,exports.PathTreeify=class{applyFilter(e,t){return!(!this.fileVisible&&!i.isDirectory(e))&&(!this.userFilter||this.userFilter({name:t,dirPath:r.dirname(e)}))}constructor({filter:e,base:t,fileVisible:r}){if(this.fileVisible=!1,"boolean"==typeof r&&r&&(this.fileVisible=r),void 0!==e&&(this.validateFilter(e),this.userFilter=e),!t||!i.isValid(t))throw new Error(`${t} is not a valid path!`);if(!i.isDirectory(t))throw new Error(`${t} is not a dirPath!`);this.base=t}validateFilter(e){if("function"!=typeof e)throw new TypeError("filter must be a function")}initNode(){return new s}buildChildren(e,s){const n=[],o=t.readdirSync(e);for(const t of o){const o=r.join(e,t);if(!this.applyFilter(o,t))continue;const l=this.initNode();l.value=t,l.parent=s,n.push(l),this.fileVisible&&i.isFile(o)?l.type=exports.PathTreeNodeType.File:(l.type=exports.PathTreeNodeType.Dir,l.children=this.buildChildren(o,l))}return n}checkRelativePaths(e){if(!Array.isArray(e))throw new Error("Expected array, got "+typeof 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=r.resolve(this.base,s);if(!i.isValid(n))throw new Error(`Path does not exist or is not accessible: ${n} (from relative path: ${s})`);if(!(i.isDirectory(n)||this.fileVisible&&i.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(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(),s=this.formatSegments(e);this.checkRelativePaths(s);for(const e of s){const s=r.resolve(this.base,e),n=this.initNode();n.value=e,n.parent=t,t.children.push(n),this.fileVisible&&i.isFile(s)?n.type=exports.PathTreeNodeType.File:(n.type=exports.PathTreeNodeType.Dir,n.children=this.buildChildren(s,n))}return t}buildByFilter(e){const t=this.getAllEntriesUnderBase();return this.buildBySegments(t.filter(e))}getPathBy(e){const t=e.getPath();return{relative:t,absolute:r.resolve(this.base,t)}}buildBy(e){if(Array.isArray(e))return this.buildBySegments(e);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)}};
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 i,constants as e,statSync as r}from"fs";import{sep as s,dirname as n,join as l,resolve as o}from"path";class a{static isValid(t){try{return i(t,e.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 h;!function(t){t.Dir="dir",t.File="file",t.Unknown="unknown"}(h||(h={}));class c{constructor(){this.parent=null,this.value="",this.children=[],this.type=h.Unknown}getPath(){let t="",i=this;for(;i.parent;)t=t?`${i.value}${s}${t}`:i.value,i=i.parent;return t}}class u{applyFilter(t,i){return!(!this.fileVisible&&!a.isDirectory(t))&&(!this.userFilter||this.userFilter({name:i,dirPath:n(t)}))}constructor({filter:t,base:i,fileVisible:e}){if(this.fileVisible=!1,"boolean"==typeof e&&e&&(this.fileVisible=e),void 0!==t&&(this.validateFilter(t),this.userFilter=t),!i||!a.isValid(i))throw new Error(`${i} is not a valid path!`);if(!a.isDirectory(i))throw new Error(`${i} is not a dirPath!`);this.base=i}validateFilter(t){if("function"!=typeof t)throw new TypeError("filter must be a function")}initNode(){return new c}buildChildren(i,e){const r=[],s=t(i);for(const t of s){const s=l(i,t);if(!this.applyFilter(s,t))continue;const n=this.initNode();n.value=t,n.parent=e,r.push(n),this.fileVisible&&a.isFile(s)?n.type=h.File:(n.type=h.Dir,n.children=this.buildChildren(s,n))}return r}checkRelativePaths(t){if(!Array.isArray(t))throw new Error("Expected array, got "+typeof t);for(let i=0;i<t.length;i++){const e=t[i];if("string"!=typeof e)throw new Error(`Item at index ${i} is not a string, got ${typeof e}`);const r=o(this.base,e);if(!a.isValid(r))throw new Error(`Path does not exist or is not accessible: ${r} (from relative path: ${e})`);if(!(a.isDirectory(r)||this.fileVisible&&a.isFile(r)))throw new Error(`Path is not a directory: ${r} (from relative path: ${e})`)}}formatSegments(t){return t.map(t=>t.split(/[/\\]/).filter(Boolean).join(s)).filter(Boolean)}getAllEntriesUnderBase(){return t(this.base).filter(t=>{const i=o(this.base,t);return this.applyFilter(i,t)})}buildBySegments(t){const i=this.initNode(),e=this.formatSegments(t);this.checkRelativePaths(e);for(const t of e){const e=o(this.base,t),r=this.initNode();r.value=t,r.parent=i,i.children.push(r),this.fileVisible&&a.isFile(e)?r.type=h.File:(r.type=h.Dir,r.children=this.buildChildren(e,r))}return i}buildByFilter(t){const i=this.getAllEntriesUnderBase();return this.buildBySegments(i.filter(t))}getPathBy(t){const i=t.getPath();return{relative:i,absolute:o(this.base,i)}}buildBy(t){if(Array.isArray(t))return this.buildBySegments(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.getAllEntriesUnderBase();return this.buildBySegments(t)}}export{c as PathTreeNode,h as PathTreeNodeType,u 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 {};
@@ -11,16 +11,22 @@ 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
+ usePathCache?: boolean;
14
15
  }
15
16
  /** Classification of a node in the path tree */
16
- export declare enum PathTreeNodeType {
17
+ export declare enum PathTreeNodeKind {
17
18
  Dir = "dir",
18
19
  File = "file",
19
20
  /** Assigned before the node's type has been resolved */
20
21
  Unknown = "unknown"
21
22
  }
22
- /** Represents a single entry (directory or file) in the path tree */
23
- export declare class PathTreeNode {
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;
24
30
  /** Reference to the parent node; null for the root node */
25
31
  parent: PathTreeNode | null;
26
32
  /** The entry name of this node (not a full path) */
@@ -28,17 +34,27 @@ export declare class PathTreeNode {
28
34
  /** Child nodes; non-empty only for directory nodes */
29
35
  children: PathTreeNode[];
30
36
  /** Whether this node is a directory, a file, or not yet resolved */
31
- type: PathTreeNodeType;
37
+ type: PathTreeNodeKind;
32
38
  /**
33
- * Walks up the parent chain to compute this node's relative path from the tree root.
34
- * @returns The relative path string using the platform separator
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
35
42
  */
36
- getPath(): string;
43
+ getPath(): {
44
+ relative: string;
45
+ absolute: string;
46
+ };
37
47
  }
38
48
  /** Builds a tree of {@link PathTreeNode} entries rooted at a given base path */
39
49
  export declare class PathTreeify {
40
50
  /** The root directory to scan */
41
51
  private base;
52
+ /**
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;
42
58
  /**
43
59
  * Optional user-supplied filter. When set, every entry must pass this predicate
44
60
  * in addition to the built-in visibility check.
@@ -46,6 +62,7 @@ export declare class PathTreeify {
46
62
  private userFilter?;
47
63
  /** When true, files are included as leaf nodes during traversal. Defaults to false */
48
64
  private fileVisible;
65
+ constructor(props: Partial<PathTreeifyProps>);
49
66
  /**
50
67
  * Determines whether a given entry should be included in the tree.
51
68
  * - If {@link fileVisible} is false, non-directory entries are always excluded.
@@ -54,27 +71,40 @@ export declare class PathTreeify {
54
71
  * @param name - Entry name (filename or directory name)
55
72
  */
56
73
  private applyFilter;
57
- constructor({ filter, base, fileVisible }: Partial<PathTreeifyProps>);
58
74
  /**
59
75
  * Asserts that the provided value is a callable {@link FilterFunction}.
60
76
  * Throws a TypeError if the check fails.
61
77
  */
62
78
  private validateFilter;
63
- /** Creates a new unattached {@link PathTreeNode} */
79
+ /**
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.
84
+ */
64
85
  private initNode;
65
86
  /**
66
87
  * Recursively reads {@link dirPath} and builds child nodes for each entry that
67
88
  * passes {@link applyFilter}. Directories are traversed depth-first;
68
89
  * files (when {@link fileVisible} is true) become leaf nodes.
69
- * @param dirPath - Absolute path of the directory to read
70
- * @param parent - The parent node to attach child nodes to
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
71
100
  */
72
101
  private buildChildren;
73
102
  /**
74
103
  * Validates that every entry in {@link relativeSegments} refers to an accessible
75
104
  * path under {@link base}. When {@link fileVisible} is false, each path must be
76
105
  * a directory; when true, regular files are also accepted.
77
- * @param relativeSegments - Relative path strings to validate
106
+ * @param relativeSegments - Relative path strings to validate; assumed to be a string
107
+ * array (callers are responsible for type safety at the boundary)
78
108
  */
79
109
  private checkRelativePaths;
80
110
  /**
@@ -90,29 +120,23 @@ export declare class PathTreeify {
90
120
  */
91
121
  private getAllEntriesUnderBase;
92
122
  /**
93
- * Builds a subtree containing only the entries identified by {@link segments}.
94
- * Paths are normalised via {@link formatSegments} and validated before use.
95
- * @param segments - Relative paths to include as top-level nodes
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
96
128
  */
97
129
  private buildBySegments;
98
130
  /**
99
- * Builds a subtree from top-level entries whose names satisfy {@link filter}.
100
- * Note: this predicate only affects top-level selection, not recursive traversal.
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.
101
133
  * For recursive filtering use the `filter` constructor option.
102
- * @param filter - Predicate applied to each top-level entry name
134
+ * @param filter - Predicate applied to each top-depth entry name
103
135
  */
104
136
  private buildByFilter;
105
- /**
106
- * Returns the relative and absolute paths for a given node by delegating to
107
- * {@link PathTreeNode.getPath} and resolving against {@link base}.
108
- */
109
- getPathBy(node: PathTreeNode): {
110
- relative: string;
111
- absolute: string;
112
- };
113
137
  /** Overload: build the tree from an explicit list of relative path segments */
114
138
  buildBy(segments: string[]): PathTreeNode;
115
- /** Overload: build the tree from a predicate applied to top-level entry names */
139
+ /** Overload: build the tree from a predicate applied to top-depth entry names */
116
140
  buildBy(filter: (segment: string) => boolean): PathTreeNode;
117
141
  /** Builds a full tree from all immediate entries under the base path */
118
142
  build(): PathTreeNode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-treeify",
3
- "version": "1.3.0-beta.56a60fd",
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",