filter-scan-dir 2.1.3 → 2.1.5
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 +105 -1
- package/dist/index.d.ts +32 -14
- package/dist/index.js +121 -46
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
[![npm badge][npm-badge-png]][package-url]
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Extremely fast recursive directory crawling and filtering for Node.js.
|
|
11
|
+
Returns a flat array of file paths.
|
|
12
|
+
It can scan 1 million files in about half a second with a warm OS cache and `fullStat: false`.
|
|
13
|
+
See [Performance](#performance) for the measured setup.
|
|
11
14
|
|
|
12
15
|
- Supports super fast concurrent mode in async version.
|
|
13
16
|
|
|
@@ -34,8 +37,109 @@ console.log(await filterScanDir({ cwd: "test" }));
|
|
|
34
37
|
|
|
35
38
|
- **[API Docs]**
|
|
36
39
|
|
|
40
|
+
# Filtering
|
|
41
|
+
|
|
42
|
+
| Option | Purpose |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `ignoreDirs` | Skip exact directory basenames at every depth. Accepts a string or array. |
|
|
45
|
+
| `prefilter` | Reject entries before `lstat`. Requires `fullStat: true`. |
|
|
46
|
+
| `filterExt` | Include only matching extensions. |
|
|
47
|
+
| `ignoreExt` | Exclude matching extensions. |
|
|
48
|
+
| `filter` | Decide whether to include each file. |
|
|
49
|
+
| `filterDir` | Skip directories before scanning their children. |
|
|
50
|
+
|
|
51
|
+
`filter` and `filterDir` receive `(name, relativeDir, extras)`.
|
|
52
|
+
Return `true` to accept an entry or `false` to skip it.
|
|
53
|
+
Directories enter the output only with `includeDir: true`.
|
|
54
|
+
Return `{ stop: true }` to stop the scan.
|
|
55
|
+
With `grouping: true`, return a string to choose a result group.
|
|
56
|
+
|
|
57
|
+
Use `prefilter` when only some entries need full metadata:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const files = await filterScanDir({
|
|
61
|
+
cwd: ".",
|
|
62
|
+
fullStat: true,
|
|
63
|
+
ignoreDirs: ["node_modules", ".git"],
|
|
64
|
+
prefilter: (name, _dir, entry) => entry.isDirectory() || name.endsWith(".ts"),
|
|
65
|
+
filter: (_name, _dir, { stat }) => stat.size < 100_000,
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`prefilter` receives `(name, relativeDir, Dirent)` and runs synchronously.
|
|
70
|
+
Return `false` to reject an entry. Rejecting a directory skips its entire subtree.
|
|
71
|
+
`prefilter` with `fullStat: false` throws before scanning, regardless of `rethrowError`.
|
|
72
|
+
|
|
73
|
+
The order is `ignoreDirs`, `prefilter`, extension filters, `lstat`, then `filter` or `filterDir`.
|
|
74
|
+
Extension filters apply only to non-directory entries.
|
|
75
|
+
Early rejections avoid `lstat`, so rejected entries cannot report `lstat` errors.
|
|
76
|
+
`ignoreDirs` matches names, not paths or glob patterns. Symlinks are never followed.
|
|
77
|
+
|
|
78
|
+
# Performance
|
|
79
|
+
|
|
80
|
+
`fullStat: false` gets entry types from `readdir` as `Dirent` objects.
|
|
81
|
+
This avoids a separate `lstat` call for every file and directory entry.
|
|
82
|
+
In our warm-cache tests, it was **2.3× faster on the [fynmesh repo](https://www.fynmesh.win)** than `fullStat: true`.
|
|
83
|
+
It was **8.1× faster on the 1,000 × 1,000 synthetic tree**.
|
|
84
|
+
|
|
85
|
+
Use it when names and entry types are enough:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const files = await filterScanDir({ cwd: "src", fullStat: false });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Filter callbacks receive `Dirent` objects instead of `Stats` objects.
|
|
92
|
+
Sizes, timestamps, and permissions are unavailable.
|
|
93
|
+
|
|
94
|
+
The default concurrency is `50`.
|
|
95
|
+
Higher concurrency is not always faster. `concurrency: Infinity` removes the limit.
|
|
96
|
+
More concurrent reads can increase memory use. Benchmark your directory tree before changing it.
|
|
97
|
+
`filterScanDirSync` blocks the event loop.
|
|
98
|
+
|
|
99
|
+
Keep Dirent mode for filters that only need names or entry types:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const files = await filterScanDir({
|
|
103
|
+
cwd: ".",
|
|
104
|
+
fullStat: false,
|
|
105
|
+
filterExt: [".js", ".ts"],
|
|
106
|
+
ignoreDirs: ["node_modules", ".git"],
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `filterDir` can skip entire subtrees.
|
|
111
|
+
- Extension filters still read every visited directory. Rejected entries skip `lstat` in full-stat mode.
|
|
112
|
+
- Custom callbacks add work per entry.
|
|
113
|
+
- `sortFiles: true` adds sorting work per directory.
|
|
114
|
+
|
|
115
|
+
Use the default `fullStat: true` for full metadata.
|
|
116
|
+
The extra `lstat` calls can make scans much slower.
|
|
117
|
+
The cost is higher on cold caches or slow storage.
|
|
118
|
+
|
|
119
|
+
Scan times depend on storage, directory layout, and OS cache state.
|
|
120
|
+
Warm-cache measurements do not predict cold-cache performance.
|
|
121
|
+
Scanning lists entries without reading file contents.
|
|
122
|
+
|
|
123
|
+
## Example measurements
|
|
124
|
+
|
|
125
|
+
These are medians from seven warm-cache runs.
|
|
126
|
+
Both modes used concurrency `50`.
|
|
127
|
+
No filters or sorting were enabled. Symlinks were excluded.
|
|
128
|
+
|
|
129
|
+
Test system: Node.js 22.22.2 on macOS, Apple M4 Pro, 24 GB RAM.
|
|
130
|
+
|
|
131
|
+
| Tree | Files | Async `fullStat: false` | Async `fullStat: true` |
|
|
132
|
+
| --- | ---: | ---: | ---: |
|
|
133
|
+
| Synthetic: 1,000 directories with 1,000 empty files each | 1,000,000 | 551 ms | 4,483 ms |
|
|
134
|
+
| [fynmesh repo](https://www.fynmesh.win) with installed dependencies: 48,347 directories | 292,242 | 949 ms | 2,157 ms |
|
|
135
|
+
|
|
136
|
+
The fynmesh repo scan included `node_modules` and `.git`.
|
|
137
|
+
Its directory count includes the root.
|
|
138
|
+
|
|
37
139
|
# License
|
|
38
140
|
|
|
141
|
+
Copyright (c) 2022-2026 Joel Chen
|
|
142
|
+
|
|
39
143
|
Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
|
40
144
|
|
|
41
145
|
[npm-version-svg]: https://badge.fury.io/js/filter-scan-dir.svg
|
package/dist/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { Dirent, Stats } from "fs";
|
|
|
2
2
|
/**
|
|
3
3
|
* type of the 3rd argument for the filter callback
|
|
4
4
|
*/
|
|
5
|
-
export type ExtrasData = {
|
|
5
|
+
export type ExtrasData<Stat extends Dirent | Stats = Dirent | Stats> = {
|
|
6
6
|
/** name of the file being considered */
|
|
7
7
|
file: string;
|
|
8
8
|
/** path to the directory being processed that contains the file */
|
|
9
9
|
path: string;
|
|
10
10
|
/** result from fs.lstat or readdir */
|
|
11
|
-
stat:
|
|
11
|
+
stat: Stat;
|
|
12
12
|
/** full path with cwd + path + file */
|
|
13
13
|
fullFile: string;
|
|
14
14
|
/** path to file without cwd: path + file */
|
|
@@ -28,7 +28,7 @@ export type FilterInfo = {
|
|
|
28
28
|
group?: string;
|
|
29
29
|
/** if `true` then skip the file or directory, else add it */
|
|
30
30
|
skip?: boolean;
|
|
31
|
-
/** stop
|
|
31
|
+
/** stop all filtering; async scans drain reads already in flight before returning. */
|
|
32
32
|
stop?: boolean;
|
|
33
33
|
/** if not `undefined`, then use this as the value to add to the output */
|
|
34
34
|
formatName?: string;
|
|
@@ -49,11 +49,11 @@ export type FilterResult = boolean | string | FilterInfo;
|
|
|
49
49
|
* first level directory.
|
|
50
50
|
* @param extras - extras data
|
|
51
51
|
*/
|
|
52
|
-
export type FilterCallback = (file: string, path: string, extras: ExtrasData) => FilterResult;
|
|
52
|
+
export type FilterCallback<Stat extends Dirent | Stats = Dirent | Stats> = (file: string, path: string, extras: ExtrasData<Stat>) => FilterResult;
|
|
53
53
|
/**
|
|
54
54
|
* Options for filterScanDir
|
|
55
55
|
*/
|
|
56
|
-
export type Options = {
|
|
56
|
+
export type Options<FullStat extends boolean = boolean> = {
|
|
57
57
|
/** current working directory to start scanning */
|
|
58
58
|
cwd?: string;
|
|
59
59
|
/**
|
|
@@ -79,9 +79,10 @@ export type Options = {
|
|
|
79
79
|
* - *Default*: `true` - for significant performance improvement, set this to `false`
|
|
80
80
|
*
|
|
81
81
|
*/
|
|
82
|
-
fullStat?:
|
|
82
|
+
fullStat?: FullStat;
|
|
83
83
|
/**
|
|
84
|
-
* for async version only -
|
|
84
|
+
* for async version only - maximum directories reading entries or metadata concurrently.
|
|
85
|
+
* *Default*: `50`. Ancestors waiting for child directories do not consume a slot.
|
|
85
86
|
*
|
|
86
87
|
* - Set this to `0` or `1` to disable concurrent mode
|
|
87
88
|
*
|
|
@@ -93,9 +94,16 @@ export type Options = {
|
|
|
93
94
|
/** set to `true` to throw errors instead of ignoring them */
|
|
94
95
|
rethrowError?: boolean;
|
|
95
96
|
/** callback to filter files. */
|
|
96
|
-
filter?: FilterCallback
|
|
97
|
+
filter?: FilterCallback<FullStat extends false ? Dirent : Stats>;
|
|
97
98
|
/** callback to filter directories. */
|
|
98
|
-
filterDir?: FilterCallback
|
|
99
|
+
filterDir?: FilterCallback<FullStat extends false ? Dirent : Stats>;
|
|
100
|
+
/** directory basenames to skip at every depth, before reading metadata or children */
|
|
101
|
+
ignoreDirs?: string | string[];
|
|
102
|
+
/**
|
|
103
|
+
* synchronous entry filter before lstat; returning false also prunes directories.
|
|
104
|
+
* Requires fullStat to be true (the default). Throws when fullStat is false.
|
|
105
|
+
*/
|
|
106
|
+
prefilter?: (file: string, path: string, entry: Dirent) => boolean;
|
|
99
107
|
/** array or string of extensions to ignore. ext must include `.`, ie: `".js"` */
|
|
100
108
|
ignoreExt?: string | string[];
|
|
101
109
|
/** array or string of extensions to include only, apply after `ignoreExt` */
|
|
@@ -107,17 +115,19 @@ export type Options = {
|
|
|
107
115
|
* - If you didn't specify this, then `cwd` is automatically converted to use `/`.
|
|
108
116
|
*/
|
|
109
117
|
pathSep?: string;
|
|
110
|
-
}
|
|
118
|
+
} & ([FullStat] extends [false] ? {
|
|
119
|
+
fullStat: false;
|
|
120
|
+
} : {});
|
|
111
121
|
/**
|
|
112
122
|
* options specifically to set grouping flag `true` to enable grouping of files
|
|
113
123
|
*/
|
|
114
|
-
export type GroupingOptions = {
|
|
124
|
+
export type GroupingOptions<FullStat extends boolean = boolean> = {
|
|
115
125
|
/**
|
|
116
126
|
* enable grouping of files
|
|
117
127
|
* This is default to disabled, so it's only expecting `true` to enable it.
|
|
118
128
|
*/
|
|
119
129
|
grouping: true;
|
|
120
|
-
} & Options
|
|
130
|
+
} & Options<FullStat>;
|
|
121
131
|
/**
|
|
122
132
|
* The scanned result if grouping is enabled.
|
|
123
133
|
*
|
|
@@ -133,9 +143,17 @@ export type GroupingResult = {
|
|
|
133
143
|
* @returns
|
|
134
144
|
*/
|
|
135
145
|
export declare function filterScanDir(options?: string): Promise<string[]>;
|
|
146
|
+
export declare function filterScanDir(options: GroupingOptions<true>): Promise<GroupingResult>;
|
|
147
|
+
export declare function filterScanDir(options: GroupingOptions<false>): Promise<GroupingResult>;
|
|
148
|
+
export declare function filterScanDir(options: GroupingOptions): Promise<GroupingResult>;
|
|
149
|
+
export declare function filterScanDir(options: Options<true>): Promise<string[]>;
|
|
150
|
+
export declare function filterScanDir(options: Options<false>): Promise<string[]>;
|
|
136
151
|
export declare function filterScanDir(options?: Options): Promise<string[]>;
|
|
137
|
-
export declare function filterScanDir(options?: GroupingOptions): Promise<GroupingResult>;
|
|
138
152
|
/** sync version of filter scan dir */
|
|
139
153
|
export declare function filterScanDirSync(options?: string): string[];
|
|
154
|
+
export declare function filterScanDirSync(options: GroupingOptions<true>): GroupingResult;
|
|
155
|
+
export declare function filterScanDirSync(options: GroupingOptions<false>): GroupingResult;
|
|
156
|
+
export declare function filterScanDirSync(options: GroupingOptions): GroupingResult;
|
|
157
|
+
export declare function filterScanDirSync(options: Options<true>): string[];
|
|
158
|
+
export declare function filterScanDirSync(options: Options<false>): string[];
|
|
140
159
|
export declare function filterScanDirSync(options?: Options): string[];
|
|
141
|
-
export declare function filterScanDirSync(options?: GroupingOptions): GroupingResult;
|
package/dist/index.js
CHANGED
|
@@ -95,12 +95,6 @@ function processFile(options, extras) {
|
|
|
95
95
|
extras.stat.isSymbolicLink()) {
|
|
96
96
|
return false;
|
|
97
97
|
}
|
|
98
|
-
if (options.ignoreExt.length > 0 && options.ignoreExt.indexOf(extras.ext) >= 0) {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
if (options.filterExt.length > 0 && options.filterExt.indexOf(extras.ext) < 0) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
98
|
const filterResult = options.filter ? options.filter(extras.file, extras.path, extras) : true; // default to include
|
|
105
99
|
if (filterResult) {
|
|
106
100
|
if (filterResult.skip !== true) {
|
|
@@ -110,6 +104,25 @@ function processFile(options, extras) {
|
|
|
110
104
|
}
|
|
111
105
|
return false;
|
|
112
106
|
}
|
|
107
|
+
// Reject entries before allocating callback extras or requesting full metadata.
|
|
108
|
+
function acceptEntry(options, entry, path) {
|
|
109
|
+
const isDirectory = entry.isDirectory();
|
|
110
|
+
if (isDirectory && options._ignoreDirs.has(entry.name))
|
|
111
|
+
return false;
|
|
112
|
+
if (options.prefilter && !options.prefilter(entry.name, path, entry))
|
|
113
|
+
return false;
|
|
114
|
+
if (isDirectory)
|
|
115
|
+
return true;
|
|
116
|
+
if (options.ignoreExt.length || options.filterExt.length) {
|
|
117
|
+
const ix = entry.name.lastIndexOf(".");
|
|
118
|
+
const ext = ix > 0 ? entry.name.substring(ix) : "";
|
|
119
|
+
if (options.ignoreExt.indexOf(ext) >= 0)
|
|
120
|
+
return false;
|
|
121
|
+
if (options.filterExt.length && options.filterExt.indexOf(ext) < 0)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
113
126
|
/**
|
|
114
127
|
* get the result base on grouping enable flag
|
|
115
128
|
*
|
|
@@ -118,7 +131,7 @@ function processFile(options, extras) {
|
|
|
118
131
|
*/
|
|
119
132
|
function getResult(options) {
|
|
120
133
|
return options.grouping
|
|
121
|
-
?
|
|
134
|
+
? { files: [], ...options.result }
|
|
122
135
|
: options.result.files || [];
|
|
123
136
|
}
|
|
124
137
|
/**
|
|
@@ -135,7 +148,7 @@ function walkSync(path, options, level = 0) {
|
|
|
135
148
|
const dir = Path.join(options.dir, path);
|
|
136
149
|
let files = Fs.readdirSync(dir, options.readdirOpts);
|
|
137
150
|
if (options.sortFiles) {
|
|
138
|
-
if (options.
|
|
151
|
+
if (!options.readdirOpts) {
|
|
139
152
|
files = files.sort();
|
|
140
153
|
}
|
|
141
154
|
else {
|
|
@@ -143,15 +156,20 @@ function walkSync(path, options, level = 0) {
|
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
const dirs = [];
|
|
146
|
-
|
|
159
|
+
const extrasFiles = options.fullStat && options._earlyFilter && (options.filter || options.filterDir)
|
|
160
|
+
? files.map((entry) => entry.name)
|
|
161
|
+
: files;
|
|
147
162
|
// process files first
|
|
148
|
-
for (let ix = 0; !
|
|
163
|
+
for (let ix = 0; !options._stopped && ix < files.length; ix++) {
|
|
149
164
|
const file = files[ix];
|
|
165
|
+
if (options._earlyFilter && !acceptEntry(options, file, path))
|
|
166
|
+
continue;
|
|
150
167
|
let extras;
|
|
151
168
|
if (options.fullStat) {
|
|
152
|
-
const
|
|
169
|
+
const name = options._earlyFilter ? file.name : file;
|
|
170
|
+
const fullFile = join2(options._sep, dir, name);
|
|
153
171
|
const stat = Fs.lstatSync(fullFile);
|
|
154
|
-
extras = makeExtrasData(
|
|
172
|
+
extras = makeExtrasData(name, fullFile, path, stat, extrasFiles, options);
|
|
155
173
|
}
|
|
156
174
|
else {
|
|
157
175
|
const fullFile = join2(options._sep, dir, file.name);
|
|
@@ -161,15 +179,16 @@ function walkSync(path, options, level = 0) {
|
|
|
161
179
|
dirs.push(extras);
|
|
162
180
|
}
|
|
163
181
|
else {
|
|
164
|
-
|
|
182
|
+
options._stopped = !!processFile(options, extras);
|
|
165
183
|
}
|
|
166
184
|
}
|
|
167
185
|
// now process dirs
|
|
168
|
-
if (!
|
|
169
|
-
for (let ix = 0; ix < dirs.length; ix++) {
|
|
186
|
+
if (!options._stopped && dirs.length > 0) {
|
|
187
|
+
for (let ix = 0; !options._stopped && ix < dirs.length; ix++) {
|
|
170
188
|
const extras = dirs[ix];
|
|
171
189
|
const flags = processDir(options, extras);
|
|
172
190
|
if (flags.stop) {
|
|
191
|
+
options._stopped = true;
|
|
173
192
|
break;
|
|
174
193
|
}
|
|
175
194
|
if (!flags.skip && level < options.maxLevel) {
|
|
@@ -183,10 +202,24 @@ function walkSync(path, options, level = 0) {
|
|
|
183
202
|
throw err;
|
|
184
203
|
}
|
|
185
204
|
}
|
|
186
|
-
return getResult(options);
|
|
205
|
+
return level === 0 ? getResult(options) : undefined;
|
|
187
206
|
}
|
|
188
207
|
const asyncReaddir = Util.promisify(Fs.readdir);
|
|
189
208
|
const asyncLStat = Util.promisify(Fs.lstat);
|
|
209
|
+
// Transfer a directory slot to the next waiter without shifting the queue.
|
|
210
|
+
function releaseDirectory(options) {
|
|
211
|
+
if (options._waitIndex < options._waiting.length) {
|
|
212
|
+
const resume = options._waiting[options._waitIndex++];
|
|
213
|
+
if (options._waitIndex === options._waiting.length) {
|
|
214
|
+
options._waiting = [];
|
|
215
|
+
options._waitIndex = 0;
|
|
216
|
+
}
|
|
217
|
+
resume();
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
options._concurrentCount--;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
190
223
|
/**
|
|
191
224
|
* async version of dir walk
|
|
192
225
|
*
|
|
@@ -196,12 +229,25 @@ const asyncLStat = Util.promisify(Fs.lstat);
|
|
|
196
229
|
* @returns
|
|
197
230
|
*/
|
|
198
231
|
async function walk(path, options, level = 0) {
|
|
232
|
+
let promises = [];
|
|
233
|
+
let hasSlot = false;
|
|
199
234
|
try {
|
|
235
|
+
if (options.concurrency > 1) {
|
|
236
|
+
if (options._concurrentCount >= options.concurrency) {
|
|
237
|
+
await new Promise((resolve) => options._waiting.push(resolve));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
options._concurrentCount++;
|
|
241
|
+
}
|
|
242
|
+
hasSlot = true;
|
|
243
|
+
}
|
|
244
|
+
if (options._stopped)
|
|
245
|
+
return undefined;
|
|
200
246
|
// Use Path.join to normalize the directory path once at entry
|
|
201
247
|
const dir = Path.join(options.dir, path);
|
|
202
248
|
let files = await asyncReaddir(dir, options.readdirOpts);
|
|
203
249
|
if (options.sortFiles) {
|
|
204
|
-
if (options.
|
|
250
|
+
if (!options.readdirOpts) {
|
|
205
251
|
files = files.sort();
|
|
206
252
|
}
|
|
207
253
|
else {
|
|
@@ -209,15 +255,22 @@ async function walk(path, options, level = 0) {
|
|
|
209
255
|
}
|
|
210
256
|
}
|
|
211
257
|
const dirs = [];
|
|
212
|
-
|
|
258
|
+
const extrasFiles = options.fullStat && options._earlyFilter && (options.filter || options.filterDir)
|
|
259
|
+
? files.map((entry) => entry.name)
|
|
260
|
+
: files;
|
|
213
261
|
// process files first
|
|
214
|
-
for (let ix = 0; !
|
|
262
|
+
for (let ix = 0; !options._stopped && ix < files.length; ix++) {
|
|
215
263
|
const file = files[ix];
|
|
264
|
+
if (options._earlyFilter && !acceptEntry(options, file, path))
|
|
265
|
+
continue;
|
|
216
266
|
let extras;
|
|
217
267
|
if (options.fullStat) {
|
|
218
|
-
const
|
|
268
|
+
const name = options._earlyFilter ? file.name : file;
|
|
269
|
+
const fullFile = join2(options._sep, dir, name);
|
|
219
270
|
const stat = await asyncLStat(fullFile);
|
|
220
|
-
|
|
271
|
+
if (options._stopped)
|
|
272
|
+
break;
|
|
273
|
+
extras = makeExtrasData(name, fullFile, path, stat, extrasFiles, options);
|
|
221
274
|
}
|
|
222
275
|
else {
|
|
223
276
|
const fullFile = join2(options._sep, dir, file.name);
|
|
@@ -227,33 +280,31 @@ async function walk(path, options, level = 0) {
|
|
|
227
280
|
dirs.push(extras);
|
|
228
281
|
}
|
|
229
282
|
else {
|
|
230
|
-
|
|
283
|
+
options._stopped = !!processFile(options, extras);
|
|
231
284
|
}
|
|
232
285
|
}
|
|
286
|
+
// The slot covers readdir and all lstats, but not waiting for descendants.
|
|
287
|
+
if (hasSlot) {
|
|
288
|
+
releaseDirectory(options);
|
|
289
|
+
hasSlot = false;
|
|
290
|
+
}
|
|
233
291
|
// now process dirs
|
|
234
|
-
if (!
|
|
235
|
-
let
|
|
236
|
-
for (let ix = 0; ix < dirs.length; ix++) {
|
|
292
|
+
if (!options._stopped && dirs.length > 0) {
|
|
293
|
+
for (let ix = 0; !options._stopped && ix < dirs.length; ix++) {
|
|
237
294
|
const extras = dirs[ix];
|
|
238
295
|
const flags = processDir(options, extras);
|
|
239
296
|
if (flags.stop) {
|
|
297
|
+
options._stopped = true;
|
|
240
298
|
break;
|
|
241
299
|
}
|
|
242
300
|
if (!flags.skip && level < options.maxLevel) {
|
|
243
301
|
const walkP = walk(extras.dirFile, options, level + 1);
|
|
244
302
|
if (options.concurrency > 1) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
else if (promises.length) {
|
|
303
|
+
promises.push(walkP);
|
|
304
|
+
// Bound eager child walks as well as active directory operations.
|
|
305
|
+
if (promises.length >= options.concurrency) {
|
|
250
306
|
await Promise.all(promises);
|
|
251
|
-
|
|
252
|
-
promises = [walkP];
|
|
253
|
-
options._concurrentCount++;
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
await walkP;
|
|
307
|
+
promises = [];
|
|
257
308
|
}
|
|
258
309
|
}
|
|
259
310
|
else {
|
|
@@ -261,19 +312,27 @@ async function walk(path, options, level = 0) {
|
|
|
261
312
|
}
|
|
262
313
|
}
|
|
263
314
|
}
|
|
264
|
-
if (promises.length) {
|
|
265
|
-
await Promise.all(promises);
|
|
266
|
-
options._concurrentCount -= promises.length;
|
|
267
|
-
promises = [];
|
|
268
|
-
}
|
|
269
315
|
}
|
|
270
316
|
}
|
|
271
317
|
catch (err) {
|
|
272
318
|
if (options.rethrowError) {
|
|
273
|
-
|
|
319
|
+
// Child walks resolve after recording failure, so none can reject unobserved.
|
|
320
|
+
if (!options._error)
|
|
321
|
+
options._error = { cause: err };
|
|
322
|
+
options._stopped = true;
|
|
274
323
|
}
|
|
275
324
|
}
|
|
276
|
-
|
|
325
|
+
finally {
|
|
326
|
+
if (hasSlot)
|
|
327
|
+
releaseDirectory(options);
|
|
328
|
+
// A callback can throw after siblings have started. Drain them before returning.
|
|
329
|
+
if (promises.length) {
|
|
330
|
+
await Promise.all(promises);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (level === 0 && options._error)
|
|
334
|
+
throw options._error.cause;
|
|
335
|
+
return level === 0 ? getResult(options) : undefined;
|
|
277
336
|
}
|
|
278
337
|
/**
|
|
279
338
|
* make a copy of the user's options with proper defaults
|
|
@@ -282,7 +341,11 @@ async function walk(path, options, level = 0) {
|
|
|
282
341
|
* @returns
|
|
283
342
|
*/
|
|
284
343
|
function makeOptions(opts) {
|
|
285
|
-
|
|
344
|
+
// Walkers select the matching callback metadata from fullStat at runtime.
|
|
345
|
+
const options = (typeof opts === "string" ? { cwd: opts } : opts);
|
|
346
|
+
if (options.prefilter && options.fullStat === false) {
|
|
347
|
+
throw new TypeError("prefilter requires fullStat: true");
|
|
348
|
+
}
|
|
286
349
|
const sep = options.pathSep || Path.posix.sep;
|
|
287
350
|
let cwd = options.cwd || options.dir || process.cwd();
|
|
288
351
|
if (!options.hasOwnProperty("pathSep") && cwd.includes("\\")) {
|
|
@@ -314,7 +377,8 @@ function makeOptions(opts) {
|
|
|
314
377
|
grouping: undefined,
|
|
315
378
|
}, options, {
|
|
316
379
|
dir: cwd,
|
|
317
|
-
|
|
380
|
+
fullStat: options.fullStat === undefined ? true : options.fullStat,
|
|
381
|
+
result: Object.create(null),
|
|
318
382
|
ignoreExt: []
|
|
319
383
|
.concat(options.ignoreExt)
|
|
320
384
|
.map(cleanExt)
|
|
@@ -324,8 +388,19 @@ function makeOptions(opts) {
|
|
|
324
388
|
.map(cleanExt)
|
|
325
389
|
.filter((x) => x),
|
|
326
390
|
_concurrentCount: 0,
|
|
391
|
+
_waiting: [],
|
|
392
|
+
_waitIndex: 0,
|
|
393
|
+
_stopped: false,
|
|
394
|
+
_error: undefined,
|
|
395
|
+
_earlyFilter: false,
|
|
396
|
+
_ignoreDirs: new Set([].concat(options.ignoreDirs || [])),
|
|
327
397
|
});
|
|
328
|
-
|
|
398
|
+
opts2._earlyFilter =
|
|
399
|
+
!!opts2.prefilter ||
|
|
400
|
+
opts2._ignoreDirs.size > 0 ||
|
|
401
|
+
opts2.ignoreExt.length > 0 ||
|
|
402
|
+
opts2.filterExt.length > 0;
|
|
403
|
+
if (!opts2.fullStat || opts2._earlyFilter) {
|
|
329
404
|
opts2.readdirOpts = { withFileTypes: true };
|
|
330
405
|
}
|
|
331
406
|
return opts2;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "filter-scan-dir",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.5",
|
|
4
|
+
"description": "Extremely fast recursive directory crawling and filtering for Node.js.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"main": "./index.cjs",
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
"homepage": "https://github.com/jchip/fynjs/tree/main/packages/filter-scan-dir",
|
|
63
63
|
"dependencies": {},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@fynjs/run": "^1.1.
|
|
65
|
+
"@fynjs/run": "^1.1.5",
|
|
66
66
|
"@types/node": "^26.4.1",
|
|
67
67
|
"@vitest/coverage-v8": "^5.0.0",
|
|
68
68
|
"@vitest/ui": "^5.0.0",
|
|
69
69
|
"prettier": "^3.5.3",
|
|
70
|
-
"publish-util": "^3.1.
|
|
71
|
-
"run-verify": "^2.1.
|
|
70
|
+
"publish-util": "^3.1.4",
|
|
71
|
+
"run-verify": "^2.1.5",
|
|
72
72
|
"typedoc": "^0.28.14",
|
|
73
73
|
"typescript": "^7.0.2",
|
|
74
74
|
"vite": "^8.2.2",
|