rrdir 14.1.2 → 14.2.1

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,11 +1,19 @@
1
1
  # rrdir
2
2
  [![](https://img.shields.io/npm/v/rrdir.svg?style=flat)](https://www.npmjs.org/package/rrdir) [![](https://img.shields.io/npm/dm/rrdir.svg)](https://www.npmjs.org/package/rrdir) [![](https://packagephobia.com/badge?p=rrdir)](https://packagephobia.com/result?p=rrdir) [![](https://depx.co/api/badge/rrdir)](https://depx.co/pkg/rrdir)
3
3
 
4
- > Recursive directory reader with a delightful API
5
-
6
4
  `rrdir` recursively reads a directory and returns entries within via an async iterator or async/sync as Array. It can typically iterate millions of files in a matter of seconds. Memory usage is `O(1)` for the async iterator and `O(n)` for the Array variants.
7
5
 
8
- Contrary to other similar modules, this module is optionally able to read any path including ones that contain invalid UTF-8 sequences.
6
+ This module is able to read any path including ones that contain invalid UTF-8 sequences.
7
+
8
+ | Benchmark | rrdir | fdir |
9
+ |---|---|---|
10
+ | async | 55ms | 55ms |
11
+ | sync | 150ms | 173ms |
12
+ | async + exclude | 46ms | — |
13
+ | sync + exclude | 117ms | — |
14
+ | async iterator | 313ms | — |
15
+
16
+ Results for 122K entries (111K files, 11K dirs), Node.js on macOS. rrdir returns richer entries (path + directory + symlink) while fdir returns only paths. Run with `make bench`.
9
17
 
10
18
  ## Usage
11
19
  ```console
package/dist/index.d.ts CHANGED
@@ -11,12 +11,6 @@ type RRDirOpts = {
11
11
  exclude?: Array<string>;
12
12
  insensitive?: boolean;
13
13
  };
14
- type Matcher = ((path: string) => boolean) | null;
15
- type InternalOpts = {
16
- includeMatcher: Matcher;
17
- excludeMatcher: Matcher;
18
- encoding: Encoding;
19
- };
20
14
  type Entry<T = Dir> = {
21
15
  /** The path to the entry, will be relative if `dir` is given relative. If `dir` is a `Uint8Array`, this will be too. Always present. */path: T; /** Boolean indicating whether the entry is a directory. `undefined` on error. */
22
16
  directory?: boolean; /** Boolean indicating whether the entry is a symbolic link. `undefined` on error. */
@@ -24,7 +18,7 @@ type Entry<T = Dir> = {
24
18
  stats?: Stats; /** Any error encountered while reading this entry. `undefined` on success. */
25
19
  err?: Error;
26
20
  };
27
- declare function rrdir<T extends Dir>(dir: T, opts?: RRDirOpts, internalOpts?: InternalOpts): AsyncGenerator<Entry<T>>;
21
+ declare function rrdir<T extends Dir>(dir: T, opts?: RRDirOpts): AsyncGenerator<Entry<T>>;
28
22
  declare function rrdirAsync<T extends Dir>(dir: T, opts?: RRDirOpts): Promise<Array<Entry<T>>>;
29
23
  declare function rrdirSync<T extends Dir>(dir: T, opts?: RRDirOpts): Array<Entry<T>>;
30
24
  //#endregion
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { lstat, readdir, stat } from "node:fs/promises";
2
2
  import { lstatSync, readdirSync, statSync } from "node:fs";
3
- import { resolve, sep } from "node:path";
3
+ import { isAbsolute, resolve, sep } from "node:path";
4
4
 
5
5
  //#region index.ts
6
6
  const encoder = new TextEncoder();
@@ -30,13 +30,13 @@ function makePath({ name }, dir, encoding) {
30
30
  return result;
31
31
  } else return dir === "." ? name : `${dir}${sep}${name}`;
32
32
  }
33
- function build(dirent, path, stats, opts) {
33
+ function build(path, isDir, isSym, stats, needStats) {
34
34
  const entry = {
35
35
  path,
36
- directory: (stats || dirent).isDirectory(),
37
- symlink: (stats || dirent).isSymbolicLink()
36
+ directory: stats ? stats.isDirectory() : isDir,
37
+ symlink: stats ? stats.isSymbolicLink() : isSym
38
38
  };
39
- if (opts.stats) entry.stats = stats;
39
+ if (needStats) entry.stats = stats;
40
40
  return entry;
41
41
  }
42
42
  function globToRegex(pattern, insensitive) {
@@ -52,9 +52,10 @@ function globToRegex(pattern, insensitive) {
52
52
  function createMatcher(patterns, insensitive) {
53
53
  if (!patterns?.length) return null;
54
54
  const regexes = patterns.map((pattern) => globToRegex(pattern, insensitive));
55
+ const cwdPrefix = resolve(".") + sep;
55
56
  return (path) => {
56
- const resolved = resolve(path);
57
- const normalizedPath = isWindows ? resolved.replace(/\\/g, "/") : resolved;
57
+ const absolute = isAbsolute(path) ? path : cwdPrefix + path;
58
+ const normalizedPath = isWindows ? absolute.replace(/\\/g, "/") : absolute;
58
59
  return regexes.some((regex) => regex.test(normalizedPath));
59
60
  };
60
61
  }
@@ -78,24 +79,62 @@ function initOpts(dir, opts) {
78
79
  internalOpts: {
79
80
  includeMatcher,
80
81
  excludeMatcher,
81
- encoding
82
+ hasMatcher: Boolean(excludeMatcher || includeMatcher),
83
+ encoding,
84
+ followSymlinks: Boolean(opts.followSymlinks),
85
+ needStats: Boolean(opts.stats),
86
+ strict: Boolean(opts.strict),
87
+ readdirOpts: {
88
+ encoding,
89
+ withFileTypes: true
90
+ }
82
91
  }
83
92
  };
84
93
  }
85
94
  function getStringPath(path, encoding) {
86
95
  return encoding === "buffer" ? toString(path) : path;
87
96
  }
88
- async function* rrdir(dir, opts = {}, internalOpts) {
89
- if (!internalOpts) ({dir, opts, internalOpts} = initOpts(dir, opts));
90
- const { includeMatcher, excludeMatcher, encoding } = internalOpts;
97
+ async function* rrdir(dir, opts = {}) {
98
+ const init = initOpts(dir, opts);
99
+ const { hasMatcher, encoding, followSymlinks, needStats, strict, readdirOpts } = init.internalOpts;
100
+ dir = init.dir;
101
+ if (!hasMatcher && !followSymlinks && !needStats) {
102
+ const stack = [dir];
103
+ while (stack.length > 0) {
104
+ const currentDir = stack.pop();
105
+ let dirents;
106
+ try {
107
+ dirents = await readdir(currentDir, readdirOpts);
108
+ } catch (err) {
109
+ if (strict) throw err;
110
+ yield {
111
+ path: currentDir,
112
+ err
113
+ };
114
+ continue;
115
+ }
116
+ for (const dirent of dirents) {
117
+ const path = makePath(dirent, currentDir, encoding);
118
+ const isDir = dirent.isDirectory();
119
+ yield {
120
+ path,
121
+ directory: isDir,
122
+ symlink: dirent.isSymbolicLink()
123
+ };
124
+ if (isDir) stack.push(path);
125
+ }
126
+ }
127
+ return;
128
+ }
129
+ yield* rrdirInner(dir, init.opts, init.internalOpts);
130
+ }
131
+ async function* rrdirInner(dir, opts, internalOpts) {
132
+ const { includeMatcher, excludeMatcher, hasMatcher, encoding, followSymlinks, needStats, strict, readdirOpts } = internalOpts;
91
133
  let dirents = [];
92
134
  try {
93
- dirents = await readdir(dir, {
94
- encoding,
95
- withFileTypes: true
96
- });
135
+ dirents = await readdir(dir, readdirOpts);
97
136
  } catch (err) {
98
- if (opts.strict) throw err;
137
+ if (strict) throw err;
99
138
  yield {
100
139
  path: dir,
101
140
  err
@@ -104,31 +143,36 @@ async function* rrdir(dir, opts = {}, internalOpts) {
104
143
  if (!dirents.length) return;
105
144
  for (const dirent of dirents) {
106
145
  const path = makePath(dirent, dir, encoding);
107
- const stringPath = getStringPath(path, encoding);
108
- if (excludeMatcher?.(stringPath)) continue;
109
- const isSymbolicLink = Boolean(opts.followSymlinks && dirent.isSymbolicLink());
110
- const isIncluded = !includeMatcher || includeMatcher(stringPath);
146
+ let isIncluded = true;
147
+ if (hasMatcher) {
148
+ const stringPath = getStringPath(path, encoding);
149
+ if (excludeMatcher?.(stringPath)) continue;
150
+ isIncluded = !includeMatcher || includeMatcher(stringPath);
151
+ }
152
+ const isDir = dirent.isDirectory();
153
+ const isSym = dirent.isSymbolicLink();
154
+ const isFollowedSym = followSymlinks && isSym;
111
155
  let stats;
112
156
  if (isIncluded) {
113
- if (opts.stats || isSymbolicLink) try {
114
- stats = await (opts.followSymlinks ? stat : lstat)(path);
157
+ if (needStats || isFollowedSym) try {
158
+ stats = await (followSymlinks ? stat : lstat)(path);
115
159
  } catch (err) {
116
- if (opts.strict) throw err;
160
+ if (strict) throw err;
117
161
  yield {
118
162
  path,
119
163
  err
120
164
  };
121
165
  }
122
- yield build(dirent, path, stats, opts);
166
+ yield build(path, isDir, isSym, stats, needStats);
123
167
  }
124
- let recurse = false;
125
- if (isSymbolicLink) {
168
+ let recurse = isDir;
169
+ if (isFollowedSym) {
126
170
  if (!stats) try {
127
171
  stats = await stat(path);
128
172
  } catch {}
129
- if (stats?.isDirectory()) recurse = true;
130
- } else if (dirent.isDirectory()) recurse = true;
131
- if (recurse) yield* rrdir(path, opts, internalOpts);
173
+ recurse = Boolean(stats?.isDirectory());
174
+ }
175
+ if (recurse) yield* rrdirInner(path, opts, internalOpts);
132
176
  }
133
177
  }
134
178
  async function rrdirAsync(dir, opts = {}) {
@@ -138,49 +182,53 @@ async function rrdirAsync(dir, opts = {}) {
138
182
  return results;
139
183
  }
140
184
  async function rrdirAsyncInner(dir, opts, internalOpts, results) {
141
- const { includeMatcher, excludeMatcher, encoding } = internalOpts;
185
+ const { includeMatcher, excludeMatcher, hasMatcher, encoding, followSymlinks, needStats, strict, readdirOpts } = internalOpts;
142
186
  let dirents = [];
143
187
  try {
144
- dirents = await readdir(dir, {
145
- encoding,
146
- withFileTypes: true
147
- });
188
+ dirents = await readdir(dir, readdirOpts);
148
189
  } catch (err) {
149
- if (opts.strict) throw err;
190
+ if (strict) throw err;
150
191
  results.push({
151
192
  path: dir,
152
193
  err
153
194
  });
154
195
  }
155
196
  if (!dirents.length) return;
156
- await Promise.all(dirents.map(async (dirent) => {
197
+ const pendingDirs = [];
198
+ for (const dirent of dirents) {
157
199
  const path = makePath(dirent, dir, encoding);
158
- const stringPath = getStringPath(path, encoding);
159
- if (excludeMatcher?.(stringPath)) return;
160
- const isSymbolicLink = Boolean(opts.followSymlinks && dirent.isSymbolicLink());
161
- const isIncluded = !includeMatcher || includeMatcher(stringPath);
200
+ let isIncluded = true;
201
+ if (hasMatcher) {
202
+ const stringPath = getStringPath(path, encoding);
203
+ if (excludeMatcher?.(stringPath)) continue;
204
+ isIncluded = !includeMatcher || includeMatcher(stringPath);
205
+ }
206
+ const isDir = dirent.isDirectory();
207
+ const isSym = dirent.isSymbolicLink();
208
+ const isFollowedSym = followSymlinks && isSym;
162
209
  let stats;
163
210
  if (isIncluded) {
164
- if (opts.stats || isSymbolicLink) try {
165
- stats = await (opts.followSymlinks ? stat : lstat)(path);
211
+ if (needStats || isFollowedSym) try {
212
+ stats = await (followSymlinks ? stat : lstat)(path);
166
213
  } catch (err) {
167
- if (opts.strict) throw err;
214
+ if (strict) throw err;
168
215
  results.push({
169
216
  path,
170
217
  err
171
218
  });
172
219
  }
173
- results.push(build(dirent, path, stats, opts));
220
+ results.push(build(path, isDir, isSym, stats, needStats));
174
221
  }
175
- let recurse = false;
176
- if (isSymbolicLink) {
222
+ let recurse = isDir;
223
+ if (isFollowedSym) {
177
224
  if (!stats) try {
178
225
  stats = await stat(path);
179
226
  } catch {}
180
- if (stats?.isDirectory()) recurse = true;
181
- } else if (dirent.isDirectory()) recurse = true;
182
- if (recurse) await rrdirAsyncInner(path, opts, internalOpts, results);
183
- }));
227
+ recurse = Boolean(stats?.isDirectory());
228
+ }
229
+ if (recurse) pendingDirs.push(path);
230
+ }
231
+ if (pendingDirs.length) await Promise.all(pendingDirs.map((p) => rrdirAsyncInner(p, opts, internalOpts, results)));
184
232
  }
185
233
  function rrdirSync(dir, opts = {}) {
186
234
  const init = initOpts(dir, opts);
@@ -189,15 +237,12 @@ function rrdirSync(dir, opts = {}) {
189
237
  return results;
190
238
  }
191
239
  function rrdirSyncInner(dir, opts, internalOpts, results) {
192
- const { includeMatcher, excludeMatcher, encoding } = internalOpts;
240
+ const { includeMatcher, excludeMatcher, hasMatcher, encoding, followSymlinks, needStats, strict, readdirOpts } = internalOpts;
193
241
  let dirents = [];
194
242
  try {
195
- dirents = readdirSync(dir, {
196
- encoding,
197
- withFileTypes: true
198
- });
243
+ dirents = readdirSync(dir, readdirOpts);
199
244
  } catch (err) {
200
- if (opts.strict) throw err;
245
+ if (strict) throw err;
201
246
  results.push({
202
247
  path: dir,
203
248
  err
@@ -206,30 +251,35 @@ function rrdirSyncInner(dir, opts, internalOpts, results) {
206
251
  if (!dirents.length) return;
207
252
  for (const dirent of dirents) {
208
253
  const path = makePath(dirent, dir, encoding);
209
- const stringPath = getStringPath(path, encoding);
210
- if (excludeMatcher?.(stringPath)) continue;
211
- const isSymbolicLink = Boolean(opts.followSymlinks && dirent.isSymbolicLink());
212
- const isIncluded = !includeMatcher || includeMatcher(stringPath);
254
+ let isIncluded = true;
255
+ if (hasMatcher) {
256
+ const stringPath = getStringPath(path, encoding);
257
+ if (excludeMatcher?.(stringPath)) continue;
258
+ isIncluded = !includeMatcher || includeMatcher(stringPath);
259
+ }
260
+ const isDir = dirent.isDirectory();
261
+ const isSym = dirent.isSymbolicLink();
262
+ const isFollowedSym = followSymlinks && isSym;
213
263
  let stats;
214
264
  if (isIncluded) {
215
- if (opts.stats || isSymbolicLink) try {
216
- stats = (opts.followSymlinks ? statSync : lstatSync)(path);
265
+ if (needStats || isFollowedSym) try {
266
+ stats = (followSymlinks ? statSync : lstatSync)(path);
217
267
  } catch (err) {
218
- if (opts.strict) throw err;
268
+ if (strict) throw err;
219
269
  results.push({
220
270
  path,
221
271
  err
222
272
  });
223
273
  }
224
- results.push(build(dirent, path, stats, opts));
274
+ results.push(build(path, isDir, isSym, stats, needStats));
225
275
  }
226
- let recurse = false;
227
- if (isSymbolicLink) {
276
+ let recurse = isDir;
277
+ if (isFollowedSym) {
228
278
  if (!stats) try {
229
279
  stats = statSync(path);
230
280
  } catch {}
231
- if (stats?.isDirectory()) recurse = true;
232
- } else if (dirent.isDirectory()) recurse = true;
281
+ recurse = Boolean(stats?.isDirectory());
282
+ }
233
283
  if (recurse) rrdirSyncInner(path, opts, internalOpts, results);
234
284
  }
235
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rrdir",
3
- "version": "14.1.2",
3
+ "version": "14.2.1",
4
4
  "description": "Recursive directory reader with a delightful API",
5
5
  "author": "silverwind <me@silverwind.io>",
6
6
  "repository": "silverwind/rrdir",
@@ -17,19 +17,20 @@
17
17
  "node": ">=22"
18
18
  },
19
19
  "devDependencies": {
20
- "@types/node": "25.1.0",
21
- "@typescript/native-preview": "7.0.0-dev.20260212.1",
22
- "eslint": "9.39.2",
23
- "eslint-config-silverwind": "120.1.2",
20
+ "@types/node": "25.3.5",
21
+ "@typescript/native-preview": "7.0.0-dev.20260306.1",
22
+ "eslint": "9.39.3",
23
+ "eslint-config-silverwind": "122.0.6",
24
+ "fdir": "6.5.0",
24
25
  "jest-extended": "7.0.0",
25
- "tsdown": "0.20.1",
26
- "tsdown-config-silverwind": "1.7.3",
26
+ "tsdown": "0.21.0",
27
+ "tsdown-config-silverwind": "2.0.0",
27
28
  "typescript": "5.9.3",
28
- "typescript-config-silverwind": "14.0.0",
29
- "updates": "17.0.8",
29
+ "typescript-config-silverwind": "15.0.0",
30
+ "updates": "17.8.2",
30
31
  "updates-config-silverwind": "1.0.3",
31
- "versions": "14.0.3",
32
+ "versions": "14.2.1",
32
33
  "vitest": "4.0.18",
33
- "vitest-config-silverwind": "10.6.1"
34
+ "vitest-config-silverwind": "10.6.3"
34
35
  }
35
36
  }