path-treeify 1.3.0-beta.d47c4d9 → 1.4.0-beta.31d8864

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
  <img alt="GitHub Created At" src="https://img.shields.io/github/created-at/isaaxite/path-treeify">
28
28
  </a>
29
29
  <a href="https://github.com/isaaxite/path-treeify">
30
- <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/isaaxite/path-treeify">
30
+ <img alt="NPM Unpacked Size" src="https://img.shields.io/npm/unpacked-size/path-treeify">
31
31
  </a>
32
32
  <a href="https://github.com/isaaxite/path-treeify/commits/main/">
33
33
  <img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/isaaxite/path-treeify">
@@ -51,10 +51,12 @@
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
53
  - 🏷️ Each node has a `type` field — `PathTreeNodeKind.Dir` or `PathTreeNodeKind.File`
54
+ - 📏 Each node has a `depth` field indicating its distance from the root
54
55
  - 👁️ `fileVisible` option includes files as leaf nodes alongside directories
55
56
  - 🔍 Optional `filter` callback applied at **every depth**, including top-level entries
56
57
  - ⚡ `build()` scans the entire `base` directory with zero configuration
57
58
  - 🎛️ `buildBy()` accepts either a path segment array or a top-level filter function
59
+ - 🗃️ `usePathCache` option caches `getPath()` results per node for repeated access
58
60
  - 📦 Ships as both ESM (`index.mjs`) and CJS (`index.cjs`) with full TypeScript types
59
61
  - 🚫 Zero runtime dependencies
60
62
 
@@ -94,12 +96,12 @@ const treeifyWithFiles = new PathTreeify({
94
96
  });
95
97
  const fullTree = treeifyWithFiles.build();
96
98
 
97
- // Check node type
99
+ // Check node type and depth
98
100
  for (const child of tree.children) {
99
101
  if (child.type === PathTreeNodeKind.Dir) {
100
- console.log('dir:', child.value);
102
+ console.log(`dir (depth ${child.depth}):`, child.value);
101
103
  } else if (child.type === PathTreeNodeKind.File) {
102
- console.log('file:', child.value);
104
+ console.log(`file (depth ${child.depth}):`, child.value);
103
105
  }
104
106
  }
105
107
  ```
@@ -112,11 +114,12 @@ for (const child of tree.children) {
112
114
 
113
115
  Creates a new instance.
114
116
 
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` |
117
+ | Option | Type | Required | Description |
118
+ |----------------|------------------------------|----------|-------------------------------------------------------------------------------------|
119
+ | `base` | `string` | ✅ | Absolute path to the root directory to scan from |
120
+ | `filter` | `FilterFunction` (see below) | ❌ | Applied at **every depth** — top-level entries included |
121
+ | `fileVisible` | `boolean` | ❌ | When `true`, files are included as leaf nodes. Defaults to `false` |
122
+ | `usePathCache` | `boolean` | ❌ | When `true`, `getPath()` results are cached on each node after the first call |
120
123
 
121
124
  `base` must exist and be a directory, otherwise the constructor throws.
122
125
 
@@ -190,6 +193,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
190
193
 
191
194
  ```ts
192
195
  interface PathTreeNode {
196
+ depth: number; // distance from root; root is 0, its children are 1, etc.
193
197
  parent: PathTreeNode | null; // null only on the synthetic root
194
198
  value: string; // entry name for this node
195
199
  children: PathTreeNode[]; // empty for file nodes
@@ -199,6 +203,8 @@ interface PathTreeNode {
199
203
  }
200
204
  ```
201
205
 
206
+ When `usePathCache: true` is set on the `PathTreeify` instance, the result of `getPath()` is cached on the node after the first call — subsequent calls return the same object reference without recomputing the parent chain.
207
+
202
208
  > ⚠️ **Circular references** — `parent` points back up the tree. Use `JSON.stringify` replacers or a library like `flatted` if you need to serialize the result.
203
209
 
204
210
  ---
@@ -266,7 +272,7 @@ const tree = treeify.buildBy(name => name !== 'node_modules' && !name.startsWith
266
272
  function printPaths(node) {
267
273
  for (const child of node.children) {
268
274
  const { absolute } = child.getPath();
269
- console.log(`[${child.type}] ${absolute}`);
275
+ console.log(`[${child.type}] depth=${child.depth} ${absolute}`);
270
276
  printPaths(child);
271
277
  }
272
278
  }
@@ -274,6 +280,22 @@ function printPaths(node) {
274
280
  printPaths(tree);
275
281
  ```
276
282
 
283
+ ### Cache `getPath()` results for repeated traversal
284
+
285
+ ```ts
286
+ const treeify = new PathTreeify({
287
+ base: '/your/project',
288
+ usePathCache: true,
289
+ });
290
+ const tree = treeify.build();
291
+
292
+ // First call walks the parent chain and caches the result
293
+ const pathA = tree.children[0].getPath();
294
+ // Subsequent calls return the cached object directly
295
+ const pathB = tree.children[0].getPath();
296
+ console.log(pathA === pathB); // true
297
+ ```
298
+
277
299
  ### CommonJS usage
278
300
 
279
301
  ```js
package/dist/index.cjs CHANGED
@@ -1 +1 @@
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.parent=null,this.value="",this.children=[],this.type=exports.PathTreeNodeKind.Unknown,this.base=e}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{applyFilter(e,t){return!(!this.fileVisible&&!r.isDirectory(e))&&(!this.userFilter||this.userFilter({name:t,dirPath:i.dirname(e)}))}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}validateFilter(e){if("function"!=typeof e)throw new TypeError("filter must be a function")}initNode(){return new s(this.base)}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 l=this.initNode();l.value=t,l.parent=s,n.push(l),this.fileVisible&&r.isFile(o)?l.type=exports.PathTreeNodeKind.File:(l.type=exports.PathTreeNodeKind.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=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)}};
1
+ "use strict";var e,t=require("fs"),r=require("path");function i(e,t){for(const r in t)Object.defineProperty(e,r,{value:t[r],writable:!1,configurable:!1,enumerable:!1})}function s(e){try{return function(e){try{const r=t.statSync(e);return r.isDirectory()?exports.PathTreeNodeKind.Dir:r.isFile()?exports.PathTreeNodeKind.File:exports.PathTreeNodeKind.Other}catch(e){if("ENOENT"!==e.code)throw e}try{return t.lstatSync(e).isSymbolicLink()?exports.PathTreeNodeKind.BrokenSymlink:exports.PathTreeNodeKind.Other}catch(e){if("ENOENT"!==e.code)throw e}return exports.PathTreeNodeKind.NotFound}(e)}catch(e){return exports.PathTreeNodeKind.Error}}exports.PathTreeNodeKind=void 0,(e=exports.PathTreeNodeKind||(exports.PathTreeNodeKind={})).Dir="dir",e.File="file",e.Unknown="unknown",e.BrokenSymlink="broken_symlink",e.Other="other",e.NotFound="not_found",e.Error="error";class n{static isValid(e){try{return t.accessSync(e,t.constants.F_OK),!0}catch{return!1}}static isDirectory(e){try{return t.statSync(e).isDirectory()}catch{return!1}}static isFile(e){try{return t.statSync(e).isFile()}catch{return!1}}}class o{constructor(e){this.depth=-1,this.parent=null,this.value="",this.children=[],this.type=exports.PathTreeNodeKind.Unknown,i(this,{_usePathCache:e.usePathCache})}getPath(){let e=this,t="";const s=()=>{let e="",i=this;for(;;){if(null===i.parent){t=i.value;break}e=e?`${i.value}${r.sep}${e}`:i.value,i=i.parent}return{relative:e,absolute:r.resolve(t,e)}};return e._usePathCache?(e._pathCache||i(this,{_pathCache:s()}),e._pathCache):s()}}exports.PathTreeify=class{constructor(e){this._base="",this._fileVisible=!1,this._usePathCache=!1;const{filter:t,base:r,fileVisible:s,usePathCache:o}=e;if(void 0!==t&&(this.validateFilter(t),i(this,{_userFilter:t})),!r||!n.isValid(r))throw new Error(`${r} is not a valid path!`);if(!n.isDirectory(r))throw new Error(`${r} is not a dirPath!`);i(this,{_usePathCache:Boolean(o),_base:r,_fileVisible:!("boolean"!=typeof s||!s)&&s})}applyFilter(e,t){return!(!this._fileVisible&&!n.isDirectory(e))&&(!this._userFilter||this._userFilter({name:t,dirPath:r.dirname(e)}))}validateFilter(e){if("function"!=typeof e)throw new TypeError("filter must be a function")}initNode(){return new o({usePathCache:this._usePathCache})}buildChildren(e,i,n){const o=[],a=i.depth+1,h=n||t.readdirSync(e);for(const t of h){const n=r.join(e,t);if(!this.applyFilter(n,t))continue;const h=s(n),l=this.initNode();l.depth=a,l.value=t,l.parent=i,o.push(l),this._fileVisible&&h===exports.PathTreeNodeKind.File?l.type=h:h===exports.PathTreeNodeKind.Dir?(l.type=h,l.children=this.buildChildren(n,l)):l.type=h}return o}checkRelativePaths(e){for(let t=0;t<e.length;t++){const i=e[t];if("string"!=typeof i)throw new Error(`Item at index ${t} is not a string, got ${typeof i}`);const s=r.resolve(this._base,i);if(!n.isValid(s))throw new Error(`Path does not exist or is not accessible: ${s} (from relative path: ${i})`);if(!(n.isDirectory(s)||this._fileVisible&&n.isFile(s)))throw new Error(`Path is not a directory: ${s} (from relative path: ${i})`)}}formatSegments(e){return e.map(e=>e.split(/[/\\]/).filter(Boolean).join(r.sep)).filter(Boolean)}getAllEntriesUnderBase(){return t.readdirSync(this._base).filter(e=>{const t=r.resolve(this._base,e);return this.applyFilter(t,e)})}buildBySegments(e){const t=this.initNode();return t.depth=0,t.value=this._base,t.type=exports.PathTreeNodeKind.Dir,t.children=this.buildChildren(this._base,t,e),t}buildByFilter(e){const t=this.getAllEntriesUnderBase();return this.buildBySegments(t.filter(e))}buildBy(e){if(Array.isArray(e)){const t=this.formatSegments(e);return this.checkRelativePaths(t),this.buildBySegments(t)}if("function"==typeof e)return this.buildByFilter(e);throw new TypeError("buildBy: expected an array of strings or a filter function, but received "+typeof e)}build(){const e=this.getAllEntriesUnderBase();return this.buildBySegments(e)}};
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{readdirSync as t,accessSync as i,constants as e,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 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(t){this.parent=null,this.value="",this.children=[],this.type=h.Unknown,this.base=t}getPath(){let t="",i=this;for(;i.parent;)t=t?`${i.value}${o}${t}`:i.value,i=i.parent;return{relative:t,absolute:l(this.base,t)}}}class u{applyFilter(t,i){return!(!this.fileVisible&&!a.isDirectory(t))&&(!this.userFilter||this.userFilter({name:i,dirPath:s(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(this.base)}buildChildren(i,e){const r=[],s=t(i);for(const t of s){const s=n(i,t);if(!this.applyFilter(s,t))continue;const l=this.initNode();l.value=t,l.parent=e,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 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=l(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(o)).filter(Boolean)}getAllEntriesUnderBase(){return t(this.base).filter(t=>{const i=l(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=l(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))}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
+ import{accessSync as t,constants as e,statSync as i,lstatSync as r,readdirSync as s}from"fs";import{sep as n,resolve as o,dirname as a,join as l}from"path";var h;function c(t,e){for(const i in e)Object.defineProperty(t,i,{value:e[i],writable:!1,configurable:!1,enumerable:!1})}function u(t){try{return function(t){try{const e=i(t);return e.isDirectory()?h.Dir:e.isFile()?h.File:h.Other}catch(t){if("ENOENT"!==t.code)throw t}try{return r(t).isSymbolicLink()?h.BrokenSymlink:h.Other}catch(t){if("ENOENT"!==t.code)throw t}return h.NotFound}(t)}catch(t){return h.Error}}!function(t){t.Dir="dir",t.File="file",t.Unknown="unknown",t.BrokenSymlink="broken_symlink",t.Other="other",t.NotFound="not_found",t.Error="error"}(h||(h={}));class f{static isValid(i){try{return t(i,e.F_OK),!0}catch{return!1}}static isDirectory(t){try{return i(t).isDirectory()}catch{return!1}}static isFile(t){try{return i(t).isFile()}catch{return!1}}}class d{constructor(t){this.depth=-1,this.parent=null,this.value="",this.children=[],this.type=h.Unknown,c(this,{_usePathCache:t.usePathCache})}getPath(){let t=this,e="";const i=()=>{let t="",i=this;for(;;){if(null===i.parent){e=i.value;break}t=t?`${i.value}${n}${t}`:i.value,i=i.parent}return{relative:t,absolute:o(e,t)}};return t._usePathCache?(t._pathCache||c(this,{_pathCache:i()}),t._pathCache):i()}}class y{constructor(t){this._base="",this._fileVisible=!1,this._usePathCache=!1;const{filter:e,base:i,fileVisible:r,usePathCache:s}=t;if(void 0!==e&&(this.validateFilter(e),c(this,{_userFilter:e})),!i||!f.isValid(i))throw new Error(`${i} is not a valid path!`);if(!f.isDirectory(i))throw new Error(`${i} is not a dirPath!`);c(this,{_usePathCache:Boolean(s),_base:i,_fileVisible:!("boolean"!=typeof r||!r)&&r})}applyFilter(t,e){return!(!this._fileVisible&&!f.isDirectory(t))&&(!this._userFilter||this._userFilter({name:e,dirPath:a(t)}))}validateFilter(t){if("function"!=typeof t)throw new TypeError("filter must be a function")}initNode(){return new d({usePathCache:this._usePathCache})}buildChildren(t,e,i){const r=[],n=e.depth+1,o=i||s(t);for(const i of o){const s=l(t,i);if(!this.applyFilter(s,i))continue;const o=u(s),a=this.initNode();a.depth=n,a.value=i,a.parent=e,r.push(a),this._fileVisible&&o===h.File?a.type=o:o===h.Dir?(a.type=o,a.children=this.buildChildren(s,a)):a.type=o}return r}checkRelativePaths(t){for(let e=0;e<t.length;e++){const i=t[e];if("string"!=typeof i)throw new Error(`Item at index ${e} is not a string, got ${typeof i}`);const r=o(this._base,i);if(!f.isValid(r))throw new Error(`Path does not exist or is not accessible: ${r} (from relative path: ${i})`);if(!(f.isDirectory(r)||this._fileVisible&&f.isFile(r)))throw new Error(`Path is not a directory: ${r} (from relative path: ${i})`)}}formatSegments(t){return t.map(t=>t.split(/[/\\]/).filter(Boolean).join(n)).filter(Boolean)}getAllEntriesUnderBase(){return s(this._base).filter(t=>{const e=o(this._base,t);return this.applyFilter(e,t)})}buildBySegments(t){const e=this.initNode();return e.depth=0,e.value=this._base,e.type=h.Dir,e.children=this.buildChildren(this._base,e,t),e}buildByFilter(t){const e=this.getAllEntriesUnderBase();return this.buildBySegments(e.filter(t))}buildBy(t){if(Array.isArray(t)){const e=this.formatSegments(t);return this.checkRelativePaths(e),this.buildBySegments(e)}if("function"==typeof t)return this.buildByFilter(t);throw new TypeError("buildBy: expected an array of strings or a filter function, but received "+typeof t)}build(){const t=this.getAllEntriesUnderBase();return this.buildBySegments(t)}}export{h as PathTreeNodeKind,y as PathTreeify};
@@ -1,45 +1,19 @@
1
- /** A filter function that determines whether an entry should be included in the tree */
2
- type FilterFunction = (params: {
3
- name: string;
4
- dirPath: string;
5
- }) => boolean;
6
- /** Constructor options for PathTreeify */
7
- interface PathTreeifyProps {
8
- /** The root directory to scan */
9
- base: string;
10
- /** Optional filter applied to every entry during recursive traversal */
11
- filter?: FilterFunction;
12
- /** When true, files are included as leaf nodes alongside directories. Defaults to false */
13
- fileVisible?: boolean;
14
- }
15
- /** 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
- export interface PathTreeNode {
23
- parent: PathTreeNode | null;
24
- value: string;
25
- children: PathTreeNode[];
26
- type: PathTreeNodeKind;
27
- getPath(): {
28
- relative: string;
29
- absolute: string;
30
- };
31
- }
1
+ import { PathTreeifyProps, PathTreeNode } from './src/types';
2
+ export { PathTreeifyProps, PathTreeNodeKind, PathTreeNode } from './src/types';
32
3
  /** Builds a tree of {@link PathTreeNode} entries rooted at a given base path */
33
4
  export declare class PathTreeify {
34
5
  /** The root directory to scan */
35
- private base;
6
+ private _base;
7
+ /** When true, files are included as leaf nodes during traversal. Defaults to false */
8
+ private _fileVisible;
9
+ /** When true, absolute paths are cached on each node after the first retrieval to speed up subsequent `getPath` calls at the cost of higher memory usage. Defaults to false */
10
+ private _usePathCache;
36
11
  /**
37
12
  * Optional user-supplied filter. When set, every entry must pass this predicate
38
13
  * in addition to the built-in visibility check.
39
14
  */
40
- private userFilter?;
41
- /** When true, files are included as leaf nodes during traversal. Defaults to false */
42
- private fileVisible;
15
+ private _userFilter?;
16
+ constructor(props: Partial<PathTreeifyProps>);
43
17
  /**
44
18
  * Determines whether a given entry should be included in the tree.
45
19
  * - If {@link fileVisible} is false, non-directory entries are always excluded.
@@ -48,27 +22,39 @@ export declare class PathTreeify {
48
22
  * @param name - Entry name (filename or directory name)
49
23
  */
50
24
  private applyFilter;
51
- constructor({ filter, base, fileVisible }: Partial<PathTreeifyProps>);
52
25
  /**
53
26
  * Asserts that the provided value is a callable {@link FilterFunction}.
54
27
  * Throws a TypeError if the check fails.
55
28
  */
56
29
  private validateFilter;
57
- /** Creates a new unattached {@link PathTreeNode} */
30
+ /**
31
+ * Creates a new unattached {@link PathTreeNode}.
32
+ * The node is created with a two-layer prototype chain:
33
+ * `node → cache → pathTreeNodeShared`. The intermediate `cache` layer is a
34
+ * per-node object that holds `_pathCache` when `usePathCache` is enabled,
35
+ * keeping the cached value isolated to each node while still inheriting `base`
36
+ * and `getPath` from `pathTreeNodeShared`.
37
+ * `depth` is initialised to `-1` and must be set by the caller.
38
+ */
58
39
  private initNode;
59
40
  /**
60
41
  * Recursively reads {@link dirPath} and builds child nodes for each entry that
61
42
  * passes {@link applyFilter}. Directories are traversed depth-first;
62
43
  * files (when {@link fileVisible} is true) become leaf nodes.
63
- * @param dirPath - Absolute path of the directory to read
64
- * @param parent - The parent node to attach child nodes to
44
+ *
45
+ * @param dirPath - Absolute path of the directory to read
46
+ * @param parent - The parent node to attach child nodes to
47
+ * @param segments - Optional explicit list of entry names to use instead of reading the
48
+ * directory from disk; used by {@link buildBySegments} to skip a
49
+ * redundant `readdirSync` when the segment list is already known
65
50
  */
66
51
  private buildChildren;
67
52
  /**
68
53
  * Validates that every entry in {@link relativeSegments} refers to an accessible
69
54
  * path under {@link base}. When {@link fileVisible} is false, each path must be
70
55
  * a directory; when true, regular files are also accepted.
71
- * @param relativeSegments - Relative path strings to validate
56
+ * @param relativeSegments - Relative path strings to validate; assumed to be a string
57
+ * array (callers are responsible for type safety at the boundary)
72
58
  */
73
59
  private checkRelativePaths;
74
60
  /**
@@ -84,23 +70,24 @@ export declare class PathTreeify {
84
70
  */
85
71
  private getAllEntriesUnderBase;
86
72
  /**
87
- * Builds a subtree containing only the entries identified by {@link segments}.
88
- * Paths are normalised via {@link formatSegments} and validated before use.
89
- * @param segments - Relative paths to include as top-level nodes
73
+ * Builds a subtree whose top-depth children correspond to {@link segments}.
74
+ * The root node is created at depth 0; children are built by delegating to
75
+ * {@link buildChildren}, passing {@link segments} directly to avoid a redundant
76
+ * `readdirSync` of the base directory.
77
+ * @param segments - Normalised and validated relative path segments
90
78
  */
91
79
  private buildBySegments;
92
80
  /**
93
- * Builds a subtree from top-level entries whose names satisfy {@link filter}.
94
- * Note: this predicate only affects top-level selection, not recursive traversal.
81
+ * Builds a subtree from top-depth entries whose names satisfy {@link filter}.
82
+ * Note: this predicate only affects top-depth selection, not recursive traversal.
95
83
  * For recursive filtering use the `filter` constructor option.
96
- * @param filter - Predicate applied to each top-level entry name
84
+ * @param filter - Predicate applied to each top-depth entry name
97
85
  */
98
86
  private buildByFilter;
99
87
  /** Overload: build the tree from an explicit list of relative path segments */
100
88
  buildBy(segments: string[]): PathTreeNode;
101
- /** Overload: build the tree from a predicate applied to top-level entry names */
89
+ /** Overload: build the tree from a predicate applied to top-depth entry names */
102
90
  buildBy(filter: (segment: string) => boolean): PathTreeNode;
103
91
  /** Builds a full tree from all immediate entries under the base path */
104
92
  build(): PathTreeNode;
105
93
  }
106
- export {};
@@ -0,0 +1,59 @@
1
+ /** A filter function that determines whether an entry should be included in the tree */
2
+ export type FilterFunction = (params: {
3
+ name: string;
4
+ dirPath: string;
5
+ }) => boolean;
6
+ /** Constructor options for PathTreeify */
7
+ export interface PathTreeifyProps {
8
+ /** The root directory to scan */
9
+ base: string;
10
+ /** Optional filter applied to every entry during recursive traversal */
11
+ filter?: FilterFunction;
12
+ /** When true, files are included as leaf nodes alongside directories. Defaults to false */
13
+ fileVisible?: boolean;
14
+ /**
15
+ * When true, the result of {@link PathTreeNode.getPath} is memoised on each node
16
+ * after the first call. Subsequent calls return the same object reference without
17
+ * re-walking the parent chain. Useful when nodes are accessed repeatedly.
18
+ * Defaults to false.
19
+ */
20
+ usePathCache?: boolean;
21
+ }
22
+ /** Classification of a node in the path tree */
23
+ export declare enum PathTreeNodeKind {
24
+ Dir = "dir",
25
+ File = "file",
26
+ /** Assigned before the node's type has been resolved */
27
+ Unknown = "unknown",
28
+ BrokenSymlink = "broken_symlink",
29
+ Other = "other",
30
+ NotFound = "not_found",
31
+ Error = "error"
32
+ }
33
+ /**
34
+ * Public interface for a node in the path tree.
35
+ * Consumers receive this type; the internal implementation class is not exported.
36
+ */
37
+ export interface PathTreeNode {
38
+ /** Distance from the root node; root itself is 0, its direct children are 1, and so on */
39
+ depth: number;
40
+ /** Reference to the parent node; null for the root node */
41
+ parent: PathTreeNode | null;
42
+ /** The entry name of this node (not a full path) */
43
+ value: string;
44
+ /** Child nodes; non-empty only for directory nodes */
45
+ children: PathTreeNode[];
46
+ /** Whether this node is a directory, a file, or not yet resolved */
47
+ type: PathTreeNodeKind;
48
+ /**
49
+ * Walks up the parent chain to compute this node's relative and absolute paths.
50
+ * When the owning {@link PathTreeify} instance was created with `usePathCache: true`,
51
+ * the result is memoised after the first call and the same object is returned on
52
+ * every subsequent call.
53
+ * @returns `relative` — path from the tree root; `absolute` — fully resolved path on disk
54
+ */
55
+ getPath(): {
56
+ relative: string;
57
+ absolute: string;
58
+ };
59
+ }
@@ -0,0 +1,38 @@
1
+ import { PathTreeNode, PathTreeNodeKind } from "./types";
2
+ /** Defines read-only properties on an object. Each property is set to the corresponding value in the props object, and is non-writable, non-configurable, and non-enumerable.
3
+ * @param obj - The target object on which to define the properties
4
+ * @param props - An object where keys are property names and values are the corresponding property values to set
5
+ */
6
+ export declare function defineReadOnlyProps(obj: any, props: {
7
+ [key: string]: any;
8
+ }): void;
9
+ /** Determines the type of a given path, classifying it as a directory, file, symbolic link, or other. If the path does not exist, it is classified as NotFound. If any unexpected error occurs during the stat/lstat operations, it will be thrown to the caller.
10
+ * @param p - The path to classify
11
+ * @returns A PathTreeNodeKind value indicating the type of the path
12
+ */
13
+ export declare function getPathType(p: string): PathTreeNodeKind;
14
+ export declare function getSafePathType(p: string): PathTreeNodeKind;
15
+ /** Utility class for validating file system paths */
16
+ export declare class PathValidator {
17
+ /** Returns true if the path exists and is accessible */
18
+ static isValid(path: string): boolean;
19
+ /** Returns true if the path points to a directory */
20
+ static isDirectory(path: string): boolean;
21
+ /** Returns true if the path points to a regular file */
22
+ static isFile(path: string): boolean;
23
+ }
24
+ export declare class PathTreeNodeImp implements PathTreeNode {
25
+ depth: number;
26
+ parent: PathTreeNode | null;
27
+ value: string;
28
+ children: PathTreeNode[];
29
+ type: PathTreeNodeKind;
30
+ constructor(props: {
31
+ usePathCache: boolean;
32
+ });
33
+ /** Retrieves the absolute and relative path represented by this node. If path caching is enabled, the result will be cached after the first retrieval to optimize subsequent calls. */
34
+ getPath(): {
35
+ relative: string;
36
+ absolute: string;
37
+ };
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "path-treeify",
3
- "version": "1.3.0-beta.d47c4d9",
3
+ "version": "1.4.0-beta.31d8864",
4
4
  "description": "Convert a path or an array of paths into a tree-structured JavaScript object, where each node has a `parent` property that holds a circular reference to its parent node.",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -26,13 +26,13 @@
26
26
  "scripts": {
27
27
  "test": "ava",
28
28
  "test:coverage": "c8 ava",
29
- "test:junit": "mkdir -p reports && ava --tap | tap-xunit --package=path-treeify > reports/junit.xml",
30
- "test:report": "rimraf reports && npm run test:coverage && npm run test:junit",
29
+ "test:md": "mkdir -p reports && ava --tap | tap-json | node ./.github/scripts/generate-report.js --title \"${npm_config_title:-AVA Test Results}\" > reports/ava-test.md",
30
+ "test:report": "rimraf reports && npm run test:coverage && npm run test:md",
31
31
  "clean": "rimraf dist",
32
32
  "build": "npm run clean && rollup -c",
33
33
  "build:dev": "NODE_ENV=development npm run build",
34
34
  "build:prod": "NODE_ENV=production npm run build",
35
- "build:watch": "nodemon --watch index.ts -e ts --exec \"npm run build:dev\"",
35
+ "dev": "nodemon --watch index.ts -e ts --exec \"npm run build:dev\"",
36
36
  "check-publish": "npm pack --dry-run",
37
37
  "publish:beta": "npm publish --tag beta --access public",
38
38
  "publish:latest": "npm publish --tag latest --access public"
@@ -93,7 +93,7 @@
93
93
  "nodemon": "^3.1.14",
94
94
  "rimraf": "^6.1.3",
95
95
  "rollup": "^4.59.0",
96
- "tap-xunit": "^2.4.1",
96
+ "tap-json": "^1.0.0",
97
97
  "tslib": "^2.8.1",
98
98
  "tsx": "^4.21.0",
99
99
  "typescript": "^5.9.3"