path-treeify 1.2.0 β†’ 1.3.0-beta.1a88b75

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,6 +35,12 @@
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>
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
+ </a>
41
+ <a href='https://coveralls.io/github/isaaxite/path-treeify'>
42
+ <img src='https://coveralls.io/repos/github/isaaxite/path-treeify/badge.svg' alt='Coverage Status' />
43
+ </a>
32
44
  </div>
33
45
 
34
46
  ---
@@ -38,9 +50,11 @@
38
50
  - 🌲 Builds a recursive tree from one or more directory paths
39
51
  - πŸ”— Each node carries a `parent` circular reference for upward traversal
40
52
  - πŸ“ Each node exposes a `getPath()` method to retrieve its own paths directly
41
- - πŸ” Optional `filter` callback to include/exclude directories during scanning
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
42
56
  - ⚑ `build()` scans the entire `base` directory with zero configuration
43
- - πŸŽ›οΈ `buildBy()` accepts either a directory name array or a filter function
57
+ - πŸŽ›οΈ `buildBy()` accepts either a path segment array or a top-level filter function
44
58
  - πŸ“¦ Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
45
59
  - 🚫 Zero runtime dependencies
46
60
 
@@ -67,18 +81,27 @@ yarn add path-treeify
67
81
  ## Quick Start
68
82
 
69
83
  ```ts
70
- import { PathTreeify } from 'path-treeify';
84
+ import { PathTreeify, PathTreeNodeKind } from 'path-treeify';
71
85
 
86
+ // Directories only (default)
72
87
  const treeify = new PathTreeify({ base: '/your/project/root' });
88
+ const tree = treeify.build();
73
89
 
74
- // Scan specific directories by name
75
- const tree = treeify.buildBy(['src', 'tests']);
76
-
77
- // Scan with a filter function over all top-level directories
78
- const filtered = treeify.buildBy(name => !name.startsWith('.') && name !== 'node_modules');
79
-
80
- // Or scan everything under base at once
81
- 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
+ }
82
105
  ```
83
106
 
84
107
  ---
@@ -89,10 +112,11 @@ const fullTree = treeify.build();
89
112
 
90
113
  Creates a new instance.
91
114
 
92
- | Option | Type | Required | Description |
93
- |----------|-------------------------------|----------|----------------------------------------------------|
94
- | `base` | `string` | βœ… | Absolute path to the root directory to scan from |
95
- | `filter` | `FilterFunction` (see below) | ❌ | Called for every directory found during deep traversal |
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` |
96
120
 
97
121
  `base` must exist and be a directory, otherwise the constructor throws.
98
122
 
@@ -100,23 +124,23 @@ Creates a new instance.
100
124
 
101
125
  ### `FilterFunction`
102
126
 
103
- Used as the `filter` option in the constructor. Applied recursively during deep traversal of the tree.
127
+ Used as the `filter` option in the constructor. Applied at every level of the tree, including the immediate children of `base`.
104
128
 
105
129
  ```ts
106
130
  type FilterFunction = (params: {
107
- name: string; // directory name (leaf segment)
131
+ name: string; // entry name (file or directory name)
108
132
  dirPath: string; // absolute path of the parent directory
109
133
  }) => boolean;
110
134
  ```
111
135
 
112
- Return `true` to **include** the directory and recurse into it; `false` to **skip** it.
136
+ Return `true` to **include** the entry; `false` to **skip** it entirely.
113
137
 
114
- **Example β€” skip hidden directories and `node_modules` at every level:**
138
+ **Example β€” exclude `node_modules` and hidden entries at every depth:**
115
139
 
116
140
  ```ts
117
141
  const treeify = new PathTreeify({
118
142
  base: '/your/project',
119
- filter: ({ name }) => !name.startsWith('.') && name !== 'node_modules',
143
+ filter: ({ name }) => name !== 'node_modules' && !name.startsWith('.'),
120
144
  });
121
145
  ```
122
146
 
@@ -124,7 +148,7 @@ const treeify = new PathTreeify({
124
148
 
125
149
  ### `build(): PathTreeNode`
126
150
 
127
- Scans **all** subdirectories directly under `base` and returns a synthetic root `PathTreeNode`. This is the zero-configuration shorthand.
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.
128
152
 
129
153
  ```ts
130
154
  const tree = treeify.build();
@@ -132,62 +156,70 @@ const tree = treeify.build();
132
156
 
133
157
  ---
134
158
 
135
- ### `buildBy(dirNames: string[]): PathTreeNode`
159
+ ### `buildBy(segments: string[]): PathTreeNode`
136
160
 
137
- 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.
138
162
 
139
163
  ```ts
140
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']);
141
169
  ```
142
170
 
143
- - Leading and trailing slashes are stripped automatically.
144
- - 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`.
145
174
 
146
- ### `buildBy(filter: (dirName: string) => boolean): PathTreeNode`
175
+ ### `buildBy(filter: (segment: string) => boolean): PathTreeNode`
147
176
 
148
- Collects all top-level subdirectories under `base`, applies the given filter function, then builds a tree from the matching names.
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.
149
178
 
150
179
  ```ts
151
180
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
152
181
  ```
153
182
 
154
- > Note: this `filter` operates only on the **top-level** directory names under `base`. For filtering at every depth, pass a `filter` to the constructor instead.
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.
155
184
 
156
185
  ---
157
186
 
158
- ### `getPathBy(node: PathTreeNode): { relative: string; absolute: string }`
187
+ ### `PathTreeNode`
159
188
 
160
- 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.
161
190
 
162
191
  ```ts
163
- const { relative, absolute } = treeify.getPathBy(node);
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
+ }
164
200
  ```
165
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
+
166
204
  ---
167
205
 
168
- ### `PathTreeNode`
206
+ ### `PathTreeNodeKind`
169
207
 
170
- `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.
171
209
 
172
210
  ```ts
173
- class PathTreeNode {
174
- parent: PathTreeNode | null; // null only on the synthetic root
175
- value: string; // directory name for this node
176
- children: PathTreeNode[];
177
-
178
- getPath(): { relative: string; absolute: string };
211
+ enum PathTreeNodeKind {
212
+ Dir = 'dir',
213
+ File = 'file',
214
+ Unknown = 'unknown', // assigned before the type is resolved
179
215
  }
180
216
  ```
181
217
 
182
- **`node.getPath()`** returns the same result as `treeify.getPathBy(node)` β€” both are available for convenience.
183
-
184
- > ⚠️ **Circular references** β€” `parent` points back up the tree. Use `JSON.stringify` replacers or a library like `flatted` if you need to serialize the result.
185
-
186
218
  ---
187
219
 
188
220
  ## Examples
189
221
 
190
- ### Scan an entire base directory
222
+ ### Directories only (default)
191
223
 
192
224
  ```ts
193
225
  import { PathTreeify } from 'path-treeify';
@@ -196,19 +228,17 @@ const treeify = new PathTreeify({ base: '/your/project' });
196
228
  const tree = treeify.build();
197
229
  ```
198
230
 
199
- ### Scan specific directories
231
+ ### Include files as leaf nodes
200
232
 
201
233
  ```ts
202
- const tree = treeify.buildBy(['src', 'tests', 'docs']);
203
- ```
204
-
205
- ### Filter top-level directories
206
-
207
- ```ts
208
- const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
234
+ const treeify = new PathTreeify({
235
+ base: '/your/project',
236
+ fileVisible: true,
237
+ });
238
+ const tree = treeify.build();
209
239
  ```
210
240
 
211
- ### Filter at every depth via constructor
241
+ ### Exclude directories at every depth via constructor filter
212
242
 
213
243
  ```ts
214
244
  const treeify = new PathTreeify({
@@ -218,13 +248,25 @@ const treeify = new PathTreeify({
218
248
  const tree = treeify.build();
219
249
  ```
220
250
 
251
+ ### Scan specific paths
252
+
253
+ ```ts
254
+ const tree = treeify.buildBy(['src', 'tests', 'docs']);
255
+ ```
256
+
257
+ ### Select top-level entries with a predicate
258
+
259
+ ```ts
260
+ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
261
+ ```
262
+
221
263
  ### Retrieve paths via `node.getPath()`
222
264
 
223
265
  ```ts
224
266
  function printPaths(node) {
225
267
  for (const child of node.children) {
226
268
  const { absolute } = child.getPath();
227
- console.log(absolute);
269
+ console.log(`[${child.type}] ${absolute}`);
228
270
  printPaths(child);
229
271
  }
230
272
  }
@@ -235,7 +277,7 @@ printPaths(tree);
235
277
  ### CommonJS usage
236
278
 
237
279
  ```js
238
- const { PathTreeify } = require('path-treeify');
280
+ const { PathTreeify, PathTreeNodeKind } = require('path-treeify');
239
281
 
240
282
  const treeify = new PathTreeify({ base: __dirname });
241
283
  const tree = treeify.build();
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var t=require("fs"),r=require("path");class e{static isValid(r){try{return t.accessSync(r,t.constants.F_OK),!0}catch{return!1}}static isDirectory(r){try{return t.statSync(r).isDirectory()}catch{return!1}}}class i{constructor(t){this.parent=null,this.value="",this.children=[],this.base=t}getPath(){let t="",e=this;for(;e.parent;)t=t?`${e.value}${r.sep}${t}`:e.value,e=e.parent;return{relative:t,absolute:r.resolve(this.base,t)}}}exports.PathTreeify=class{constructor({filter:t,base:r}){if(void 0!==t&&(this.validateFilter(t),this.filter=t),!r||!e.isValid(r))throw new Error(`${r} is not a valid path!`);if(!e.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 i(this.base);return t&&(r.parent=t),r}buildChildren(e,i){const s=t.readdirSync(e),a=[];for(const n of s){const s=r.join(e,n);if(!t.statSync(s).isDirectory())continue;if(this.filter&&!this.filter({dirPath:e,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=r.resolve(this.base,s);if(!e.isValid(a))throw new Error(`Path does not exist or is not accessible: ${a} (from relative path: ${s})`);if(!e.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=r.resolve(this.base,t);return e.isDirectory(i)})}buildByDirNames(t){const e=this.initNode();this.checkRelativePaths(t);const i=this.formatDirnames(t);for(const t of i){const i=this.initNode();i.value=t,i.parent=e,i.children=this.buildChildren(r.resolve(this.base,t),i),e.children.push(i)}return e}buildByFilter(t){const r=this.getAllDirNamesUnderBase();return this.buildByDirNames(r.filter(t))}getPathBy(t){let e="",i=t;for(;i.parent;)e=e?`${i.value}${r.sep}${e}`:i.value,i=i.parent;return{relative:e,absolute:r.resolve(this.base,e)}}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.base=e.base}getPath(){let e="",t=this;for(;t.parent;)e=e?`${t.value}${i.sep}${e}`:t.value,t=t.parent;return{relative:e,absolute:i.resolve(this.base,e)}}}exports.PathTreeify=class{constructor({filter:e,base:t,fileVisible:i}){if(this.fileVisible=!1,"boolean"==typeof i&&i&&(this.fileVisible=i),void 0!==e&&(this.validateFilter(e),this.userFilter=e),!t||!r.isValid(t))throw new Error(`${t} is not a valid path!`);if(!r.isDirectory(t))throw new Error(`${t} is not a dirPath!`);this.base=t,this.pathTreeNodeShared=new s({base:t})}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);return e.parent=null,e.value="",e.children=[],e.type=exports.PathTreeNodeKind.Unknown,e}buildChildren(e,s){const n=[],o=t.readdirSync(e);for(const t of o){const o=i.join(e,t);if(!this.applyFilter(o,t))continue;const a=this.initNode();a.value=t,a.parent=s,n.push(a),this.fileVisible&&r.isFile(o)?a.type=exports.PathTreeNodeKind.File:(a.type=exports.PathTreeNodeKind.Dir,a.children=this.buildChildren(o,a))}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=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(),s=this.formatSegments(e);this.checkRelativePaths(s);for(const e of s){const s=i.resolve(this.base,e),n=this.initNode();n.value=e,n.parent=t,t.children.push(n),this.fileVisible&&r.isFile(s)?n.type=exports.PathTreeNodeKind.File:(n.type=exports.PathTreeNodeKind.Dir,n.children=this.buildChildren(s,n))}return t}buildByFilter(e){const t=this.getAllEntriesUnderBase();return this.buildBySegments(t.filter(e))}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)}};
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)})}buildByDirNames(t){const r=this.initNode();this.checkRelativePaths(t);const e=this.formatDirnames(t);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 l,sep as o}from"path";class a{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 h;!function(t){t.Dir="dir",t.File="file",t.Unknown="unknown"}(h||(h={}));class c{constructor(t){this.base=t.base}getPath(){let t="",e=this;for(;e.parent;)t=t?`${e.value}${o}${t}`:e.value,e=e.parent;return{relative:t,absolute:l(this.base,t)}}}class u{constructor({filter:t,base:e,fileVisible:i}){if(this.fileVisible=!1,"boolean"==typeof i&&i&&(this.fileVisible=i),void 0!==t&&(this.validateFilter(t),this.userFilter=t),!e||!a.isValid(e))throw new Error(`${e} is not a valid path!`);if(!a.isDirectory(e))throw new Error(`${e} is not a dirPath!`);this.base=e,this.pathTreeNodeShared=new c({base:e})}applyFilter(t,e){return!(!this.fileVisible&&!a.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);return t.parent=null,t.value="",t.children=[],t.type=h.Unknown,t}buildChildren(e,i){const r=[],s=t(e);for(const t of s){const s=n(e,t);if(!this.applyFilter(s,t))continue;const l=this.initNode();l.value=t,l.parent=i,r.push(l),this.fileVisible&&a.isFile(s)?l.type=h.File:(l.type=h.Dir,l.children=this.buildChildren(s,l))}return r}checkRelativePaths(t){if(!Array.isArray(t))throw new Error("Expected array, got "+typeof 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=l(this.base,i);if(!a.isValid(r))throw new Error(`Path does not exist or is not accessible: ${r} (from relative path: ${i})`);if(!(a.isDirectory(r)||this.fileVisible&&a.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(o)).filter(Boolean)}getAllEntriesUnderBase(){return t(this.base).filter(t=>{const e=l(this.base,t);return this.applyFilter(e,t)})}buildBySegments(t){const e=this.initNode(),i=this.formatSegments(t);this.checkRelativePaths(i);for(const t of i){const i=l(this.base,t),r=this.initNode();r.value=t,r.parent=e,e.children.push(r),this.fileVisible&&a.isFile(i)?r.type=h.File:(r.type=h.Dir,r.children=this.buildChildren(i,r))}return e}buildByFilter(t){const e=this.getAllEntriesUnderBase();return this.buildBySegments(e.filter(t))}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{h as PathTreeNodeKind,u as PathTreeify};
@@ -1,93 +1,128 @@
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;
10
14
  }
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
+ /** Classification of a node in the path tree */
16
+ export declare enum PathTreeNodeKind {
17
+ Dir = "dir",
18
+ File = "file",
19
+ /** Assigned before the node's type has been resolved */
20
+ Unknown = "unknown"
21
+ }
22
+ /**
23
+ * Public interface for a node in the path tree.
24
+ * Consumers receive this type; the internal implementation class is not exported.
25
+ */
26
+ export interface PathTreeNode {
15
27
  /** Reference to the parent node; null for the root node */
16
28
  parent: PathTreeNode | null;
17
- /** The directory name of this node (not a full path) */
29
+ /** The entry name of this node (not a full path) */
18
30
  value: string;
19
- /** Child nodes representing subdirectories */
31
+ /** Child nodes; non-empty only for directory nodes */
20
32
  children: PathTreeNode[];
21
- constructor(base: string);
33
+ /** Whether this node is a directory, a file, or not yet resolved */
34
+ type: PathTreeNodeKind;
22
35
  /**
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
36
+ * Walks up the parent chain to compute this node's paths.
37
+ * @returns `relative` β€” path from the tree root; `absolute` β€” fully resolved path on disk
25
38
  */
26
39
  getPath(): {
27
40
  relative: string;
28
41
  absolute: string;
29
42
  };
30
43
  }
31
- /** Builds a tree of directory nodes rooted at a given base path */
44
+ /** Builds a tree of {@link PathTreeNode} entries rooted at a given base path */
32
45
  export declare class PathTreeify {
33
46
  /** The root directory to scan */
34
47
  private base;
35
- /** Optional filter applied to each directory during traversal */
36
- private filter?;
37
- constructor({ filter, base }: Partial<PathTreeifyProps>);
38
48
  /**
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.
49
+ * Shared prototype instance for nodes produced by this builder.
50
+ * All nodes created via {@link initNode} inherit `base` and `getPath` from this object,
51
+ * avoiding per-node storage of the base path string.
52
+ */
53
+ private pathTreeNodeShared;
54
+ /**
55
+ * Optional user-supplied filter. When set, every entry must pass this predicate
56
+ * in addition to the built-in visibility check.
57
+ */
58
+ private userFilter?;
59
+ /** When true, files are included as leaf nodes during traversal. Defaults to false */
60
+ private fileVisible;
61
+ constructor({ filter, base, fileVisible }: Partial<PathTreeifyProps>);
62
+ /**
63
+ * Determines whether a given entry should be included in the tree.
64
+ * - If {@link fileVisible} is false, non-directory entries are always excluded.
65
+ * - If a {@link userFilter} is set, the entry must also satisfy it.
66
+ * @param absPath - Absolute path of the entry to test
67
+ * @param name - Entry name (filename or directory name)
68
+ */
69
+ private applyFilter;
70
+ /**
71
+ * Asserts that the provided value is a callable {@link FilterFunction}.
72
+ * Throws a TypeError if the check fails.
41
73
  */
42
74
  private validateFilter;
43
75
  /**
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
76
+ * Creates a new unattached {@link PathTreeNode}.
77
+ * The node's prototype is set to {@link pathTreeNodeShared} so that `base` and
78
+ * `getPath` are inherited without being stored on each instance individually.
46
79
  */
47
80
  private initNode;
48
81
  /**
49
- * Recursively reads a directory and builds child nodes for each subdirectory.
50
- * Applies the instance-level filter if one is set.
82
+ * Recursively reads {@link dirPath} and builds child nodes for each entry that
83
+ * passes {@link applyFilter}. Directories are traversed depth-first;
84
+ * files (when {@link fileVisible} is true) become leaf nodes.
51
85
  * @param dirPath - Absolute path of the directory to read
52
- * @param parent - The parent node to attach children to
86
+ * @param parent - The parent node to attach child nodes to
53
87
  */
54
88
  private buildChildren;
55
89
  /**
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
90
+ * Validates that every entry in {@link relativeSegments} refers to an accessible
91
+ * path under {@link base}. When {@link fileVisible} is false, each path must be
92
+ * a directory; when true, regular files are also accepted.
93
+ * @param relativeSegments - Relative path strings to validate
59
94
  */
60
95
  private checkRelativePaths;
61
96
  /**
62
- * Strips leading and trailing slashes from each directory name
63
- * and removes any resulting empty strings.
97
+ * Normalises an array of path strings by splitting on both slash styles,
98
+ * dropping empty segments, and rejoining with the platform separator.
99
+ * Entries that reduce to an empty string (e.g. `"///"`) are removed.
100
+ * @param segments - Raw path strings to normalise
64
101
  */
65
- private formatDirnames;
66
- /** Returns the names of all immediate subdirectories under the base path */
67
- private getAllDirNamesUnderBase;
102
+ private formatSegments;
68
103
  /**
69
- * Builds a tree rooted at base, containing only the specified subdirectories.
70
- * @param dirNames - Relative directory names to include as top-level nodes
104
+ * Returns the names of all immediate entries under {@link base} that pass
105
+ * {@link applyFilter}.
71
106
  */
72
- private buildByDirNames;
107
+ private getAllEntriesUnderBase;
73
108
  /**
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
109
+ * Builds a subtree containing only the entries identified by {@link segments}.
110
+ * Paths are normalised via {@link formatSegments} and validated before use.
111
+ * @param segments - Relative paths to include as top-level nodes
76
112
  */
77
- private buildByFilter;
113
+ private buildBySegments;
78
114
  /**
79
- * Computes the relative and absolute paths for a given node
80
- * by walking up the parent chain.
115
+ * Builds a subtree from top-level entries whose names satisfy {@link filter}.
116
+ * Note: this predicate only affects top-level selection, not recursive traversal.
117
+ * For recursive filtering use the `filter` constructor option.
118
+ * @param filter - Predicate applied to each top-level entry name
81
119
  */
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 */
120
+ private buildByFilter;
121
+ /** Overload: build the tree from an explicit list of relative path segments */
122
+ buildBy(segments: string[]): PathTreeNode;
123
+ /** Overload: build the tree from a predicate applied to top-level entry names */
124
+ buildBy(filter: (segment: string) => boolean): PathTreeNode;
125
+ /** Builds a full tree from all immediate entries under the base path */
91
126
  build(): PathTreeNode;
92
127
  }
93
128
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-treeify",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-beta.1a88b75",
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",
@@ -24,8 +24,11 @@
24
24
  "dist/**/*.d.ts"
25
25
  ],
26
26
  "scripts": {
27
- "test": "echo \"Error: no test specified\" && exit 1",
28
- "clean": "rimraf ./dist",
27
+ "test": "ava",
28
+ "test:coverage": "c8 ava",
29
+ "test:junit": "mkdir -p reports && ava --tap | tap-xunit --package=path-treeify > reports/junit.xml",
30
+ "test:report": "rimraf reports && npm run test:coverage && npm run test:junit",
31
+ "clean": "rimraf dist",
29
32
  "build": "npm run clean && rollup -c",
30
33
  "build:dev": "NODE_ENV=development npm run build",
31
34
  "build:prod": "NODE_ENV=production npm run build",
@@ -34,6 +37,29 @@
34
37
  "publish:beta": "npm publish --tag beta --access public",
35
38
  "publish:latest": "npm publish --tag latest --access public"
36
39
  },
40
+ "ava": {
41
+ "files": [
42
+ "tests/**/*.mjs"
43
+ ],
44
+ "nodeArguments": [
45
+ "--experimental-vm-modules"
46
+ ]
47
+ },
48
+ "c8": {
49
+ "reports-dir": "reports/coverage",
50
+ "reporter": [
51
+ "html",
52
+ "lcov"
53
+ ],
54
+ "exclude": [
55
+ "tests/**",
56
+ "node_modules/**"
57
+ ],
58
+ "lines": 80,
59
+ "statements": 80,
60
+ "functions": 80,
61
+ "branches": 75
62
+ },
37
63
  "repository": {
38
64
  "type": "git",
39
65
  "url": "git+https://github.com/isaaxite/path-treeify.git"
@@ -62,10 +88,14 @@
62
88
  "@rollup/plugin-terser": "^1.0.0",
63
89
  "@rollup/plugin-typescript": "^12.3.0",
64
90
  "@types/node": "^25.5.0",
91
+ "ava": "^7.0.0",
92
+ "c8": "^11.0.0",
65
93
  "nodemon": "^3.1.14",
66
94
  "rimraf": "^6.1.3",
67
95
  "rollup": "^4.59.0",
96
+ "tap-xunit": "^2.4.1",
68
97
  "tslib": "^2.8.1",
98
+ "tsx": "^4.21.0",
69
99
  "typescript": "^5.9.3"
70
100
  },
71
101
  "engines": {