path-treeify 1.2.0-beta.db6fe0b → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,12 @@
29
29
  <a href="https://github.com/isaaxite/path-treeify/commits/main/">
30
30
  <img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/isaaxite/path-treeify">
31
31
  </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' />
34
+ </a>
35
+ <a href='https://coveralls.io/github/isaaxite/path-treeify'>
36
+ <img src='https://coveralls.io/repos/github/isaaxite/path-treeify/badge.svg' alt='Coverage Status' />
37
+ </a>
32
38
  </div>
33
39
 
34
40
  ---
@@ -38,9 +44,9 @@
38
44
  - 🌲 Builds a recursive tree from one or more directory paths
39
45
  - 🔗 Each node carries a `parent` circular reference for upward traversal
40
46
  - 📍 Each node exposes a `getPath()` method to retrieve its own paths directly
41
- - 🔍 Optional `filter` callback to include/exclude directories during scanning
47
+ - 🔍 Optional `filter` callback applied at **every depth**, including top-level directories
42
48
  - ⚡ `build()` scans the entire `base` directory with zero configuration
43
- - 🎛️ `buildBy()` accepts either a directory name array or a filter function
49
+ - 🎛️ `buildBy()` accepts either a directory name array or a top-level filter function
44
50
  - 📦 Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
45
51
  - 🚫 Zero runtime dependencies
46
52
 
@@ -74,8 +80,8 @@ const treeify = new PathTreeify({ base: '/your/project/root' });
74
80
  // Scan specific directories by name
75
81
  const tree = treeify.buildBy(['src', 'tests']);
76
82
 
77
- // Scan with a filter function over all top-level directories
78
- const filtered = treeify.buildBy(name => !name.startsWith('.') && name !== 'node_modules');
83
+ // Scan with a top-level filter function
84
+ const filtered = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
79
85
 
80
86
  // Or scan everything under base at once
81
87
  const fullTree = treeify.build();
@@ -89,10 +95,10 @@ const fullTree = treeify.build();
89
95
 
90
96
  Creates a new instance.
91
97
 
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 |
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 |
96
102
 
97
103
  `base` must exist and be a directory, otherwise the constructor throws.
98
104
 
@@ -100,7 +106,7 @@ Creates a new instance.
100
106
 
101
107
  ### `FilterFunction`
102
108
 
103
- Used as the `filter` option in the constructor. Applied recursively during deep traversal of the tree.
109
+ Used as the `filter` option in the constructor. Applied at every level of the tree, including the immediate children of `base`.
104
110
 
105
111
  ```ts
106
112
  type FilterFunction = (params: {
@@ -109,14 +115,14 @@ type FilterFunction = (params: {
109
115
  }) => boolean;
110
116
  ```
111
117
 
112
- Return `true` to **include** the directory and recurse into it; `false` to **skip** it.
118
+ Return `true` to **include** the directory and recurse into it; `false` to **skip** it entirely.
113
119
 
114
- **Example — skip hidden directories and `node_modules` at every level:**
120
+ **Example — exclude `node_modules` and hidden directories at every depth:**
115
121
 
116
122
  ```ts
117
123
  const treeify = new PathTreeify({
118
124
  base: '/your/project',
119
- filter: ({ name }) => !name.startsWith('.') && name !== 'node_modules',
125
+ filter: ({ name }) => name !== 'node_modules' && !name.startsWith('.'),
120
126
  });
121
127
  ```
122
128
 
@@ -124,7 +130,7 @@ const treeify = new PathTreeify({
124
130
 
125
131
  ### `build(): PathTreeNode`
126
132
 
127
- Scans **all** subdirectories directly under `base` and returns a synthetic root `PathTreeNode`. This is the zero-configuration shorthand.
133
+ Scans all subdirectories directly under `base` (applying the instance-level `filter` if set) and returns a synthetic root `PathTreeNode`.
128
134
 
129
135
  ```ts
130
136
  const tree = treeify.build();
@@ -145,13 +151,13 @@ const tree = treeify.buildBy(['src', 'docs', 'tests']);
145
151
 
146
152
  ### `buildBy(filter: (dirName: string) => boolean): PathTreeNode`
147
153
 
148
- Collects all top-level subdirectories under `base`, applies the given filter function, then builds a tree from the matching names.
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.
149
155
 
150
156
  ```ts
151
157
  const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
152
158
  ```
153
159
 
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.
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.
155
161
 
156
162
  ---
157
163
 
@@ -161,6 +167,8 @@ Walks a node's `parent` chain to reconstruct its full path. Equivalent to callin
161
167
 
162
168
  ```ts
163
169
  const { relative, absolute } = treeify.getPathBy(node);
170
+ // relative → e.g. 'src/components'
171
+ // absolute → e.g. '/your/project/src/components'
164
172
  ```
165
173
 
166
174
  ---
@@ -179,7 +187,7 @@ class PathTreeNode {
179
187
  }
180
188
  ```
181
189
 
182
- **`node.getPath()`** returns the same result as `treeify.getPathBy(node)` — both are available for convenience.
190
+ `node.getPath()` returns the same result as `treeify.getPathBy(node)` — both are available for convenience.
183
191
 
184
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.
185
193
 
@@ -196,26 +204,26 @@ const treeify = new PathTreeify({ base: '/your/project' });
196
204
  const tree = treeify.build();
197
205
  ```
198
206
 
199
- ### Scan specific directories
207
+ ### Exclude directories at every depth via constructor filter
200
208
 
201
209
  ```ts
202
- const tree = treeify.buildBy(['src', 'tests', 'docs']);
210
+ const treeify = new PathTreeify({
211
+ base: '/your/project',
212
+ filter: ({ name }) => name !== 'node_modules' && !name.startsWith('.'),
213
+ });
214
+ const tree = treeify.build();
203
215
  ```
204
216
 
205
- ### Filter top-level directories
217
+ ### Scan specific directories
206
218
 
207
219
  ```ts
208
- const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
220
+ const tree = treeify.buildBy(['src', 'tests', 'docs']);
209
221
  ```
210
222
 
211
- ### Filter at every depth via constructor
223
+ ### Select top-level directories with a predicate
212
224
 
213
225
  ```ts
214
- const treeify = new PathTreeify({
215
- base: '/your/project',
216
- filter: ({ name }) => name !== 'node_modules' && !name.startsWith('.'),
217
- });
218
- const tree = treeify.build();
226
+ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith('.'));
219
227
  ```
220
228
 
221
229
  ### Retrieve paths via `node.getPath()`
@@ -245,4 +253,4 @@ const tree = treeify.build();
245
253
 
246
254
  ## License
247
255
 
248
- [MIT](https://github.com/isaaxite/path-treeify/blob/main/LICENSE) © [isaaxite](https://github.com/isaaxite)
256
+ [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();this.checkRelativePaths(t);const i=this.formatDirnames(t);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 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)}};
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();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,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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-treeify",
3
- "version": "1.2.0-beta.db6fe0b",
3
+ "version": "1.3.0",
4
4
  "description": "Convert a path or an array of paths into a tree-structured JavaScript object, where each node has a `parent` property that holds a circular reference to its parent node.",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -25,8 +25,10 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "test": "ava",
28
- "test:watch": "ava --watch",
29
- "clean": "rimraf ./dist",
28
+ "test:coverage": "c8 ava",
29
+ "test:junit": "mkdir -p reports && ava --tap | tap-xunit > reports/junit.xml",
30
+ "test:report": "rimraf reports && npm run test:coverage && npm run test:junit",
31
+ "clean": "rimraf dist",
30
32
  "build": "npm run clean && rollup -c",
31
33
  "build:dev": "NODE_ENV=development npm run build",
32
34
  "build:prod": "NODE_ENV=production npm run build",
@@ -43,6 +45,21 @@
43
45
  "--experimental-vm-modules"
44
46
  ]
45
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
+ },
46
63
  "repository": {
47
64
  "type": "git",
48
65
  "url": "git+https://github.com/isaaxite/path-treeify.git"
@@ -72,9 +89,11 @@
72
89
  "@rollup/plugin-typescript": "^12.3.0",
73
90
  "@types/node": "^25.5.0",
74
91
  "ava": "^7.0.0",
92
+ "c8": "^11.0.0",
75
93
  "nodemon": "^3.1.14",
76
94
  "rimraf": "^6.1.3",
77
95
  "rollup": "^4.59.0",
96
+ "tap-xunit": "^2.4.1",
78
97
  "tslib": "^2.8.1",
79
98
  "typescript": "^5.9.3"
80
99
  },