Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e91b296

Browse filesBrowse files
mcollinaaduh95
authored andcommitted
fs: add ignore option to fs.watch
Add an `ignore` option to `fs.watch()` to filter filesystem events. Supports string globs, RegExp, functions, or arrays of these. PR-URL: #61433 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 75c06bc commit e91b296
Copy full SHA for e91b296

8 files changed

+831-6Lines changed: 831 additions & 6 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/fs.md‎

Copy file name to clipboardExpand all lines: doc/api/fs.md
+9Lines changed: 9 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1837,6 +1837,10 @@ added:
18371837
* `overflow` {string} Either `'ignore'` or `'throw'` when there are more events to be
18381838
queued than `maxQueue` allows. `'ignore'` means overflow events are dropped and a
18391839
warning is emitted, while `'throw'` means to throw an exception. **Default:** `'ignore'`.
1840+
* `ignore` {string|RegExp|Function|Array} Pattern(s) to ignore. Strings are
1841+
glob patterns (using [`minimatch`][]), RegExp patterns are tested against
1842+
the filename, and functions receive the filename and return `true` to
1843+
ignore. **Default:** `undefined`.
18401844
* Returns: {AsyncIterator} of objects with the properties:
18411845
* `eventType` {string} The type of change
18421846
* `filename` {string|Buffer|null} The name of the file changed.
@@ -4804,6 +4808,10 @@ changes:
48044808
* `encoding` {string} Specifies the character encoding to be used for the
48054809
filename passed to the listener. **Default:** `'utf8'`.
48064810
* `signal` {AbortSignal} allows closing the watcher with an AbortSignal.
4811+
* `ignore` {string|RegExp|Function|Array} Pattern(s) to ignore. Strings are
4812+
glob patterns (using [`minimatch`][]), RegExp patterns are tested against
4813+
the filename, and functions receive the filename and return `true` to
4814+
ignore. **Default:** `undefined`.
48074815
* `listener` {Function|undefined} **Default:** `undefined`
48084816
* `eventType` {string}
48094817
* `filename` {string|Buffer|null}
@@ -8764,6 +8772,7 @@ the file contents.
87648772
[`fsPromises.utimes()`]: #fspromisesutimespath-atime-mtime
87658773
[`inotify(7)`]: https://man7.org/linux/man-pages/man7/inotify.7.html
87668774
[`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
8775+
[`minimatch`]: https://github.com/isaacs/minimatch
87678776
[`util.promisify()`]: util.md#utilpromisifyoriginal
87688777
[bigints]: https://tc39.github.io/proposal-bigint
87698778
[caveats]: #caveats
Collapse file

‎lib/fs.js‎

Copy file name to clipboardExpand all lines: lib/fs.js
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2521,7 +2521,8 @@ function watch(filename, options, listener) {
25212521
watcher[watchers.kFSWatchStart](path,
25222522
options.persistent,
25232523
options.recursive,
2524-
options.encoding);
2524+
options.encoding,
2525+
options.ignore);
25252526
}
25262527

25272528
if (listener) {
Collapse file

‎lib/internal/fs/recursive_watch.js‎

Copy file name to clipboardExpand all lines: lib/internal/fs/recursive_watch.js
+14-4Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ const {
1717
},
1818
} = require('internal/errors');
1919
const { getValidatedPath } = require('internal/fs/utils');
20-
const { kFSWatchStart, StatWatcher } = require('internal/fs/watchers');
20+
const { createIgnoreMatcher, kFSWatchStart, StatWatcher } = require('internal/fs/watchers');
2121
const { kEmptyObject } = require('internal/util');
22-
const { validateBoolean, validateAbortSignal } = require('internal/validators');
22+
const { validateBoolean, validateAbortSignal, validateIgnoreOption } = require('internal/validators');
2323
const {
2424
basename: pathBasename,
2525
join: pathJoin,
@@ -44,13 +44,14 @@ class FSWatcher extends EventEmitter {
4444
#symbolicFiles = new SafeSet();
4545
#rootPath = pathResolve();
4646
#watchingFile = false;
47+
#ignoreMatcher = null;
4748

4849
constructor(options = kEmptyObject) {
4950
super();
5051

5152
assert(typeof options === 'object');
5253

53-
const { persistent, recursive, signal, encoding } = options;
54+
const { persistent, recursive, signal, encoding, ignore } = options;
5455

5556
// TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support.
5657
if (recursive != null) {
@@ -72,6 +73,9 @@ class FSWatcher extends EventEmitter {
7273
}
7374
}
7475

76+
validateIgnoreOption(ignore, 'options.ignore');
77+
this.#ignoreMatcher = createIgnoreMatcher(ignore);
78+
7579
this.#options = { persistent, recursive, signal, encoding };
7680
}
7781

@@ -118,9 +122,15 @@ class FSWatcher extends EventEmitter {
118122
}
119123

120124
const f = pathJoin(folder, file.name);
125+
const relativePath = pathRelative(this.#rootPath, f);
126+
127+
// Skip watching ignored paths entirely to avoid kernel resource pressure
128+
if (this.#ignoreMatcher?.(relativePath)) {
129+
continue;
130+
}
121131

122132
if (!this.#files.has(f)) {
123-
this.emit('change', 'rename', pathRelative(this.#rootPath, f));
133+
this.emit('change', 'rename', relativePath);
124134

125135
if (file.isSymbolicLink()) {
126136
this.#symbolicFiles.add(f);
Collapse file

‎lib/internal/fs/watchers.js‎

Copy file name to clipboardExpand all lines: lib/internal/fs/watchers.js
+71-1Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
ArrayPrototypePush,
56
ArrayPrototypeShift,
67
Error,
78
FunctionPrototypeCall,
89
ObjectDefineProperty,
910
ObjectSetPrototypeOf,
1011
PromiseWithResolvers,
12+
RegExpPrototypeExec,
1113
Symbol,
1214
} = primordials;
1315

@@ -22,6 +24,9 @@ const {
2224

2325
const {
2426
kEmptyObject,
27+
getLazy,
28+
isWindows,
29+
isMacOS,
2530
} = require('internal/util');
2631

2732
const {
@@ -48,6 +53,7 @@ const { toNamespacedPath } = require('path');
4853
const {
4954
validateAbortSignal,
5055
validateBoolean,
56+
validateIgnoreOption,
5157
validateObject,
5258
validateUint32,
5359
validateInteger,
@@ -60,6 +66,8 @@ const {
6066
},
6167
} = require('buffer');
6268

69+
const { isRegExp } = require('internal/util/types');
70+
6371
const assert = require('internal/assert');
6472

6573
const kOldStatus = Symbol('kOldStatus');
@@ -71,6 +79,50 @@ const KFSStatWatcherRefCount = Symbol('KFSStatWatcherRefCount');
7179
const KFSStatWatcherMaxRefCount = Symbol('KFSStatWatcherMaxRefCount');
7280
const kFSStatWatcherAddOrCleanRef = Symbol('kFSStatWatcherAddOrCleanRef');
7381

82+
const lazyMinimatch = getLazy(() => require('internal/deps/minimatch/index'));
83+
84+
/**
85+
* Creates an ignore matcher function from the ignore option.
86+
* @param {string | RegExp | Function | Array} ignore - The ignore patterns
87+
* @returns {Function | null} A function that returns true if filename should be ignored
88+
*/
89+
function createIgnoreMatcher(ignore) {
90+
if (ignore == null) return null;
91+
const matchers = ArrayIsArray(ignore) ? ignore : [ignore];
92+
const compiled = [];
93+
94+
for (let i = 0; i < matchers.length; i++) {
95+
const matcher = matchers[i];
96+
if (typeof matcher === 'string') {
97+
const mm = new (lazyMinimatch().Minimatch)(matcher, {
98+
__proto__: null,
99+
nocase: isWindows || isMacOS,
100+
windowsPathsNoEscape: true,
101+
nonegate: true,
102+
nocomment: true,
103+
optimizationLevel: 2,
104+
platform: process.platform,
105+
// matchBase allows patterns without slashes to match the basename
106+
// e.g., '*.log' matches 'subdir/file.log'
107+
matchBase: true,
108+
});
109+
ArrayPrototypePush(compiled, (filename) => mm.match(filename));
110+
} else if (isRegExp(matcher)) {
111+
ArrayPrototypePush(compiled, (filename) => RegExpPrototypeExec(matcher, filename) !== null);
112+
} else {
113+
// Function
114+
ArrayPrototypePush(compiled, matcher);
115+
}
116+
}
117+
118+
return (filename) => {
119+
for (let i = 0; i < compiled.length; i++) {
120+
if (compiled[i](filename)) return true;
121+
}
122+
return false;
123+
};
124+
}
125+
74126
function emitStop(self) {
75127
self.emit('stop');
76128
}
@@ -199,6 +251,7 @@ function FSWatcher() {
199251

200252
this._handle = new FSEvent();
201253
this._handle[owner_symbol] = this;
254+
this._ignoreMatcher = null;
202255

203256
this._handle.onchange = (status, eventType, filename) => {
204257
// TODO(joyeecheung): we may check self._handle.initialized here
@@ -219,6 +272,10 @@ function FSWatcher() {
219272
error.filename = filename;
220273
this.emit('error', error);
221274
} else {
275+
// Filter events if ignore matcher is set and filename is available
276+
if (filename != null && this._ignoreMatcher?.(filename)) {
277+
return;
278+
}
222279
this.emit('change', eventType, filename);
223280
}
224281
};
@@ -235,7 +292,8 @@ ObjectSetPrototypeOf(FSWatcher, EventEmitter);
235292
FSWatcher.prototype[kFSWatchStart] = function(filename,
236293
persistent,
237294
recursive,
238-
encoding) {
295+
encoding,
296+
ignore) {
239297
if (this._handle === null) { // closed
240298
return;
241299
}
@@ -246,6 +304,10 @@ FSWatcher.prototype[kFSWatchStart] = function(filename,
246304

247305
filename = getValidatedPath(filename, 'filename');
248306

307+
// Validate and create the ignore matcher
308+
validateIgnoreOption(ignore, 'options.ignore');
309+
this._ignoreMatcher = createIgnoreMatcher(ignore);
310+
249311
const err = this._handle.start(toNamespacedPath(filename),
250312
persistent,
251313
recursive,
@@ -319,13 +381,15 @@ async function* watch(filename, options = kEmptyObject) {
319381
maxQueue = 2048,
320382
overflow = 'ignore',
321383
signal,
384+
ignore,
322385
} = options;
323386

324387
validateBoolean(persistent, 'options.persistent');
325388
validateBoolean(recursive, 'options.recursive');
326389
validateInteger(maxQueue, 'options.maxQueue');
327390
validateOneOf(overflow, 'options.overflow', ['ignore', 'error']);
328391
validateAbortSignal(signal, 'options.signal');
392+
validateIgnoreOption(ignore, 'options.ignore');
329393

330394
if (encoding && !isEncoding(encoding)) {
331395
const reason = 'is invalid encoding';
@@ -336,6 +400,7 @@ async function* watch(filename, options = kEmptyObject) {
336400
throw new AbortError(undefined, { cause: signal.reason });
337401

338402
const handle = new FSEvent();
403+
const ignoreMatcher = createIgnoreMatcher(ignore);
339404
let { promise, resolve } = PromiseWithResolvers();
340405
const queue = [];
341406
const oncancel = () => {
@@ -361,6 +426,10 @@ async function* watch(filename, options = kEmptyObject) {
361426
resolve();
362427
return;
363428
}
429+
// Filter events if ignore matcher is set and filename is available
430+
if (filename != null && ignoreMatcher?.(filename)) {
431+
return;
432+
}
364433
if (queue.length < maxQueue) {
365434
ArrayPrototypePush(queue, { __proto__: null, eventType, filename });
366435
resolve();
@@ -409,6 +478,7 @@ async function* watch(filename, options = kEmptyObject) {
409478
}
410479

411480
module.exports = {
481+
createIgnoreMatcher,
412482
FSWatcher,
413483
StatWatcher,
414484
kFSWatchStart,
Collapse file

‎lib/internal/validators.js‎

Copy file name to clipboardExpand all lines: lib/internal/validators.js
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const { normalizeEncoding } = require('internal/util');
3535
const {
3636
isAsyncFunction,
3737
isArrayBufferView,
38+
isRegExp,
3839
} = require('internal/util/types');
3940
const { signals } = internalBinding('constants').os;
4041

@@ -575,6 +576,38 @@ const validateLinkHeaderValue = hideStackFrames((hints) => {
575576
);
576577
});
577578

579+
/**
580+
* Validates a single ignore option element (string, RegExp, or Function).
581+
* @param {*} value
582+
* @param {string} name
583+
*/
584+
const validateIgnoreOptionElement = hideStackFrames((value, name) => {
585+
if (typeof value === 'string') {
586+
if (value.length === 0)
587+
throw new ERR_INVALID_ARG_VALUE(name, value, 'must be a non-empty string');
588+
return;
589+
}
590+
if (isRegExp(value)) return;
591+
if (typeof value === 'function') return;
592+
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp', 'Function'], value);
593+
});
594+
595+
/**
596+
* Validates the ignore option for fs.watch.
597+
* @param {*} value
598+
* @param {string} name
599+
*/
600+
const validateIgnoreOption = hideStackFrames((value, name) => {
601+
if (value == null) return;
602+
if (ArrayIsArray(value)) {
603+
for (let i = 0; i < value.length; i++) {
604+
validateIgnoreOptionElement(value[i], `${name}[${i}]`);
605+
}
606+
return;
607+
}
608+
validateIgnoreOptionElement(value, name);
609+
});
610+
578611
// 1. Returns false for undefined and NaN
579612
// 2. Returns true for finite numbers
580613
// 3. Throws ERR_INVALID_ARG_TYPE for non-numbers
@@ -628,6 +661,7 @@ module.exports = {
628661
validateDictionary,
629662
validateEncoding,
630663
validateFunction,
664+
validateIgnoreOption,
631665
validateInt32,
632666
validateInteger,
633667
validateNumber,

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.