rrdir 14.1.2 → 14.2.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 +11 -3
- package/dist/index.d.ts +1 -7
- package/dist/index.js +118 -68
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# rrdir
|
|
2
2
|
[](https://www.npmjs.org/package/rrdir) [](https://www.npmjs.org/package/rrdir) [](https://packagephobia.com/result?p=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
|
-
|
|
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 | 64ms | 57ms |
|
|
11
|
+
| sync | 154ms | 178ms |
|
|
12
|
+
| async + exclude | 46ms | — |
|
|
13
|
+
| sync + exclude | 120ms | — |
|
|
14
|
+
| async iterator | 319ms | — |
|
|
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
|
|
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(
|
|
33
|
+
function build(path, isDir, isSym, stats, needStats) {
|
|
34
34
|
const entry = {
|
|
35
35
|
path,
|
|
36
|
-
directory:
|
|
37
|
-
symlink:
|
|
36
|
+
directory: stats ? stats.isDirectory() : isDir,
|
|
37
|
+
symlink: stats ? stats.isSymbolicLink() : isSym
|
|
38
38
|
};
|
|
39
|
-
if (
|
|
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
|
|
57
|
-
const normalizedPath = isWindows ?
|
|
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
|
-
|
|
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 = {}
|
|
89
|
-
|
|
90
|
-
const {
|
|
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 (
|
|
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
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
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 (
|
|
114
|
-
stats = await (
|
|
157
|
+
if (needStats || isFollowedSym) try {
|
|
158
|
+
stats = await (followSymlinks ? stat : lstat)(path);
|
|
115
159
|
} catch (err) {
|
|
116
|
-
if (
|
|
160
|
+
if (strict) throw err;
|
|
117
161
|
yield {
|
|
118
162
|
path,
|
|
119
163
|
err
|
|
120
164
|
};
|
|
121
165
|
}
|
|
122
|
-
yield build(
|
|
166
|
+
yield build(path, isDir, isSym, stats, needStats);
|
|
123
167
|
}
|
|
124
|
-
let recurse =
|
|
125
|
-
if (
|
|
168
|
+
let recurse = isDir;
|
|
169
|
+
if (isFollowedSym) {
|
|
126
170
|
if (!stats) try {
|
|
127
171
|
stats = await stat(path);
|
|
128
172
|
} catch {}
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
if (recurse) yield*
|
|
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 (
|
|
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
|
-
|
|
197
|
+
const pendingDirs = [];
|
|
198
|
+
for (const dirent of dirents) {
|
|
157
199
|
const path = makePath(dirent, dir, encoding);
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
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 (
|
|
165
|
-
stats = await (
|
|
211
|
+
if (needStats || isFollowedSym) try {
|
|
212
|
+
stats = await (followSymlinks ? stat : lstat)(path);
|
|
166
213
|
} catch (err) {
|
|
167
|
-
if (
|
|
214
|
+
if (strict) throw err;
|
|
168
215
|
results.push({
|
|
169
216
|
path,
|
|
170
217
|
err
|
|
171
218
|
});
|
|
172
219
|
}
|
|
173
|
-
results.push(build(
|
|
220
|
+
results.push(build(path, isDir, isSym, stats, needStats));
|
|
174
221
|
}
|
|
175
|
-
let recurse =
|
|
176
|
-
if (
|
|
222
|
+
let recurse = isDir;
|
|
223
|
+
if (isFollowedSym) {
|
|
177
224
|
if (!stats) try {
|
|
178
225
|
stats = await stat(path);
|
|
179
226
|
} catch {}
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
if (recurse)
|
|
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 (
|
|
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
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
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 (
|
|
216
|
-
stats = (
|
|
265
|
+
if (needStats || isFollowedSym) try {
|
|
266
|
+
stats = (followSymlinks ? statSync : lstatSync)(path);
|
|
217
267
|
} catch (err) {
|
|
218
|
-
if (
|
|
268
|
+
if (strict) throw err;
|
|
219
269
|
results.push({
|
|
220
270
|
path,
|
|
221
271
|
err
|
|
222
272
|
});
|
|
223
273
|
}
|
|
224
|
-
results.push(build(
|
|
274
|
+
results.push(build(path, isDir, isSym, stats, needStats));
|
|
225
275
|
}
|
|
226
|
-
let recurse =
|
|
227
|
-
if (
|
|
276
|
+
let recurse = isDir;
|
|
277
|
+
if (isFollowedSym) {
|
|
228
278
|
if (!stats) try {
|
|
229
279
|
stats = statSync(path);
|
|
230
280
|
} catch {}
|
|
231
|
-
|
|
232
|
-
}
|
|
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.
|
|
3
|
+
"version": "14.2.0",
|
|
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.
|
|
21
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
22
|
-
"eslint": "9.39.
|
|
23
|
-
"eslint-config-silverwind": "
|
|
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.
|
|
26
|
-
"tsdown-config-silverwind": "
|
|
26
|
+
"tsdown": "0.21.0",
|
|
27
|
+
"tsdown-config-silverwind": "2.0.0",
|
|
27
28
|
"typescript": "5.9.3",
|
|
28
|
-
"typescript-config-silverwind": "
|
|
29
|
-
"updates": "17.
|
|
29
|
+
"typescript-config-silverwind": "15.0.0",
|
|
30
|
+
"updates": "17.8.2",
|
|
30
31
|
"updates-config-silverwind": "1.0.3",
|
|
31
|
-
"versions": "14.
|
|
32
|
+
"versions": "14.2.1",
|
|
32
33
|
"vitest": "4.0.18",
|
|
33
|
-
"vitest-config-silverwind": "10.6.
|
|
34
|
+
"vitest-config-silverwind": "10.6.3"
|
|
34
35
|
}
|
|
35
36
|
}
|