| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b68524e commit ffbf579
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1801,6 +1801,10 @@ added: | |||
| 1801 | 1801 | * `overflow` {string} Either `'ignore'` or `'throw'` when there are more events to be | |
| 1802 | 1802 | queued than `maxQueue` allows. `'ignore'` means overflow events are dropped and a | |
| 1803 | 1803 | warning is emitted, while `'throw'` means to throw an exception. **Default:** `'ignore'`. | |
| 1804 | + * `ignore` {string|RegExp|Function|Array} Pattern(s) to ignore. Strings are | ||
| 1805 | + glob patterns (using [`minimatch`][]), RegExp patterns are tested against | ||
| 1806 | + the filename, and functions receive the filename and return `true` to | ||
| 1807 | + ignore. **Default:** `undefined`. | ||
| 1804 | 1808 | * Returns: {AsyncIterator} of objects with the properties: | |
| 1805 | 1809 | * `eventType` {string} The type of change | |
| 1806 | 1810 | * `filename` {string|Buffer|null} The name of the file changed. | |
@@ -4761,6 +4765,10 @@ changes: | |||
| 4761 | 4765 | * `encoding` {string} Specifies the character encoding to be used for the | |
| 4762 | 4766 | filename passed to the listener. **Default:** `'utf8'`. | |
| 4763 | 4767 | * `signal` {AbortSignal} allows closing the watcher with an AbortSignal. | |
| 4768 | + * `ignore` {string|RegExp|Function|Array} Pattern(s) to ignore. Strings are | ||
| 4769 | + glob patterns (using [`minimatch`][]), RegExp patterns are tested against | ||
| 4770 | + the filename, and functions receive the filename and return `true` to | ||
| 4771 | + ignore. **Default:** `undefined`. | ||
| 4764 | 4772 | * `listener` {Function|undefined} **Default:** `undefined` | |
| 4765 | 4773 | * `eventType` {string} | |
| 4766 | 4774 | * `filename` {string|Buffer|null} | |
@@ -8516,6 +8524,7 @@ the file contents. | |||
| 8516 | 8524 | [`fsPromises.utimes()`]: #fspromisesutimespath-atime-mtime | |
| 8517 | 8525 | [`inotify(7)`]: https://man7.org/linux/man-pages/man7/inotify.7.html | |
| 8518 | 8526 | [`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 | |
| 8527 | + [`minimatch`]: https://github.com/isaacs/minimatch | ||
| 8519 | 8528 | [`util.promisify()`]: util.md#utilpromisifyoriginal | |
| 8520 | 8529 | [bigints]: https://tc39.github.io/proposal-bigint | |
| 8521 | 8530 | [caveats]: #caveats | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2554,7 +2554,8 @@ function watch(filename, options, listener) { | |||
| 2554 | 2554 | watcher[watchers.kFSWatchStart](path, | |
| 2555 | 2555 | options.persistent, | |
| 2556 | 2556 | options.recursive, | |
| 2557 | - options.encoding); | ||
| 2557 | + options.encoding, | ||
| 2558 | + options.ignore); | ||
| 2558 | 2559 | } | |
| 2559 | 2560 | ||
| 2560 | 2561 | if (listener) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -17,9 +17,9 @@ const { | |||
| 17 | 17 | }, | |
| 18 | 18 | } = require('internal/errors'); | |
| 19 | 19 | const { getValidatedPath } = require('internal/fs/utils'); | |
| 20 | - const { kFSWatchStart, StatWatcher } = require('internal/fs/watchers'); | ||
| 20 | + const { createIgnoreMatcher, kFSWatchStart, StatWatcher } = require('internal/fs/watchers'); | ||
| 21 | 21 | const { kEmptyObject } = require('internal/util'); | |
| 22 | - const { validateBoolean, validateAbortSignal } = require('internal/validators'); | ||
| 22 | + const { validateBoolean, validateAbortSignal, validateIgnoreOption } = require('internal/validators'); | ||
| 23 | 23 | const { | |
| 24 | 24 | basename: pathBasename, | |
| 25 | 25 | join: pathJoin, | |
@@ -44,13 +44,14 @@ class FSWatcher extends EventEmitter { | |||
| 44 | 44 | #symbolicFiles = new SafeSet(); | |
| 45 | 45 | #rootPath = pathResolve(); | |
| 46 | 46 | #watchingFile = false; | |
| 47 | + #ignoreMatcher = null; | ||
| 47 | 48 | ||
| 48 | 49 | constructor(options = kEmptyObject) { | |
| 49 | 50 | super(); | |
| 50 | 51 | ||
| 51 | 52 | assert(typeof options === 'object'); | |
| 52 | 53 | ||
| 53 | - const { persistent, recursive, signal, encoding } = options; | ||
| 54 | + const { persistent, recursive, signal, encoding, ignore } = options; | ||
| 54 | 55 | ||
| 55 | 56 | // TODO(anonrig): Add non-recursive support to non-native-watcher for IBMi & AIX support. | |
| 56 | 57 | if (recursive != null) { | |
@@ -72,6 +73,9 @@ class FSWatcher extends EventEmitter { | |||
| 72 | 73 | } | |
| 73 | 74 | } | |
| 74 | 75 | ||
| 76 | + validateIgnoreOption(ignore, 'options.ignore'); | ||
| 77 | + this.#ignoreMatcher = createIgnoreMatcher(ignore); | ||
| 78 | + | ||
| 75 | 79 | this.#options = { persistent, recursive, signal, encoding }; | |
| 76 | 80 | } | |
| 77 | 81 | ||
@@ -118,9 +122,15 @@ class FSWatcher extends EventEmitter { | |||
| 118 | 122 | } | |
| 119 | 123 | ||
| 120 | 124 | 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 | + } | ||
| 121 | 131 | ||
| 122 | 132 | if (!this.#files.has(f)) { | |
| 123 | - this.emit('change', 'rename', pathRelative(this.#rootPath, f)); | ||
| 133 | + this.emit('change', 'rename', relativePath); | ||
| 124 | 134 | ||
| 125 | 135 | if (file.isSymbolicLink()) { | |
| 126 | 136 | this.#symbolicFiles.add(f); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,13 +1,15 @@ | |||
| 1 | 1 | 'use strict'; | |
| 2 | 2 | ||
| 3 | 3 | const { | |
| 4 | + ArrayIsArray, | ||
| 4 | 5 | ArrayPrototypePush, | |
| 5 | 6 | ArrayPrototypeShift, | |
| 6 | 7 | Error, | |
| 7 | 8 | FunctionPrototypeCall, | |
| 8 | 9 | ObjectDefineProperty, | |
| 9 | 10 | ObjectSetPrototypeOf, | |
| 10 | 11 | PromiseWithResolvers, | |
| 12 | + RegExpPrototypeExec, | ||
| 11 | 13 | Symbol, | |
| 12 | 14 | } = primordials; | |
| 13 | 15 | ||
@@ -22,6 +24,9 @@ const { | |||
| 22 | 24 | ||
| 23 | 25 | const { | |
| 24 | 26 | kEmptyObject, | |
| 27 | + getLazy, | ||
| 28 | + isWindows, | ||
| 29 | + isMacOS, | ||
| 25 | 30 | } = require('internal/util'); | |
| 26 | 31 | ||
| 27 | 32 | const { | |
@@ -48,6 +53,7 @@ const { toNamespacedPath } = require('path'); | |||
| 48 | 53 | const { | |
| 49 | 54 | validateAbortSignal, | |
| 50 | 55 | validateBoolean, | |
| 56 | + validateIgnoreOption, | ||
| 51 | 57 | validateObject, | |
| 52 | 58 | validateUint32, | |
| 53 | 59 | validateInteger, | |
@@ -60,6 +66,8 @@ const { | |||
| 60 | 66 | }, | |
| 61 | 67 | } = require('buffer'); | |
| 62 | 68 | ||
| 69 | + const { isRegExp } = require('internal/util/types'); | ||
| 70 | + | ||
| 63 | 71 | const assert = require('internal/assert'); | |
| 64 | 72 | ||
| 65 | 73 | const kOldStatus = Symbol('kOldStatus'); | |
@@ -71,6 +79,50 @@ const KFSStatWatcherRefCount = Symbol('KFSStatWatcherRefCount'); | |||
| 71 | 79 | const KFSStatWatcherMaxRefCount = Symbol('KFSStatWatcherMaxRefCount'); | |
| 72 | 80 | const kFSStatWatcherAddOrCleanRef = Symbol('kFSStatWatcherAddOrCleanRef'); | |
| 73 | 81 | ||
| 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 | + | ||
| 74 | 126 | function emitStop(self) { | |
| 75 | 127 | self.emit('stop'); | |
| 76 | 128 | } | |
@@ -199,6 +251,7 @@ function FSWatcher() { | |||
| 199 | 251 | ||
| 200 | 252 | this._handle = new FSEvent(); | |
| 201 | 253 | this._handle[owner_symbol] = this; | |
| 254 | + this._ignoreMatcher = null; | ||
| 202 | 255 | ||
| 203 | 256 | this._handle.onchange = (status, eventType, filename) => { | |
| 204 | 257 | // TODO(joyeecheung): we may check self._handle.initialized here | |
@@ -219,6 +272,10 @@ function FSWatcher() { | |||
| 219 | 272 | error.filename = filename; | |
| 220 | 273 | this.emit('error', error); | |
| 221 | 274 | } else { | |
| 275 | + // Filter events if ignore matcher is set and filename is available | ||
| 276 | + if (filename != null && this._ignoreMatcher?.(filename)) { | ||
| 277 | + return; | ||
| 278 | + } | ||
| 222 | 279 | this.emit('change', eventType, filename); | |
| 223 | 280 | } | |
| 224 | 281 | }; | |
@@ -235,7 +292,8 @@ ObjectSetPrototypeOf(FSWatcher, EventEmitter); | |||
| 235 | 292 | FSWatcher.prototype[kFSWatchStart] = function(filename, | |
| 236 | 293 | persistent, | |
| 237 | 294 | recursive, | |
| 238 | - encoding) { | ||
| 295 | + encoding, | ||
| 296 | + ignore) { | ||
| 239 | 297 | if (this._handle === null) { // closed | |
| 240 | 298 | return; | |
| 241 | 299 | } | |
@@ -246,6 +304,10 @@ FSWatcher.prototype[kFSWatchStart] = function(filename, | |||
| 246 | 304 | ||
| 247 | 305 | filename = getValidatedPath(filename, 'filename'); | |
| 248 | 306 | ||
| 307 | + // Validate and create the ignore matcher | ||
| 308 | + validateIgnoreOption(ignore, 'options.ignore'); | ||
| 309 | + this._ignoreMatcher = createIgnoreMatcher(ignore); | ||
| 310 | + | ||
| 249 | 311 | const err = this._handle.start(toNamespacedPath(filename), | |
| 250 | 312 | persistent, | |
| 251 | 313 | recursive, | |
@@ -319,13 +381,15 @@ async function* watch(filename, options = kEmptyObject) { | |||
| 319 | 381 | maxQueue = 2048, | |
| 320 | 382 | overflow = 'ignore', | |
| 321 | 383 | signal, | |
| 384 | + ignore, | ||
| 322 | 385 | } = options; | |
| 323 | 386 | ||
| 324 | 387 | validateBoolean(persistent, 'options.persistent'); | |
| 325 | 388 | validateBoolean(recursive, 'options.recursive'); | |
| 326 | 389 | validateInteger(maxQueue, 'options.maxQueue'); | |
| 327 | 390 | validateOneOf(overflow, 'options.overflow', ['ignore', 'error']); | |
| 328 | 391 | validateAbortSignal(signal, 'options.signal'); | |
| 392 | + validateIgnoreOption(ignore, 'options.ignore'); | ||
| 329 | 393 | ||
| 330 | 394 | if (encoding && !isEncoding(encoding)) { | |
| 331 | 395 | const reason = 'is invalid encoding'; | |
@@ -336,6 +400,7 @@ async function* watch(filename, options = kEmptyObject) { | |||
| 336 | 400 | throw new AbortError(undefined, { cause: signal.reason }); | |
| 337 | 401 | ||
| 338 | 402 | const handle = new FSEvent(); | |
| 403 | + const ignoreMatcher = createIgnoreMatcher(ignore); | ||
| 339 | 404 | let { promise, resolve } = PromiseWithResolvers(); | |
| 340 | 405 | const queue = []; | |
| 341 | 406 | const oncancel = () => { | |
@@ -361,6 +426,10 @@ async function* watch(filename, options = kEmptyObject) { | |||
| 361 | 426 | resolve(); | |
| 362 | 427 | return; | |
| 363 | 428 | } | |
| 429 | + // Filter events if ignore matcher is set and filename is available | ||
| 430 | + if (filename != null && ignoreMatcher?.(filename)) { | ||
| 431 | + return; | ||
| 432 | + } | ||
| 364 | 433 | if (queue.length < maxQueue) { | |
| 365 | 434 | ArrayPrototypePush(queue, { __proto__: null, eventType, filename }); | |
| 366 | 435 | resolve(); | |
@@ -409,6 +478,7 @@ async function* watch(filename, options = kEmptyObject) { | |||
| 409 | 478 | } | |
| 410 | 479 | ||
| 411 | 480 | module.exports = { | |
| 481 | + createIgnoreMatcher, | ||
| 412 | 482 | FSWatcher, | |
| 413 | 483 | StatWatcher, | |
| 414 | 484 | kFSWatchStart, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -36,6 +36,7 @@ const { normalizeEncoding } = require('internal/util'); | |||
| 36 | 36 | const { | |
| 37 | 37 | isAsyncFunction, | |
| 38 | 38 | isArrayBufferView, | |
| 39 | + isRegExp, | ||
| 39 | 40 | } = require('internal/util/types'); | |
| 40 | 41 | const { signals } = internalBinding('constants').os; | |
| 41 | 42 | ||
@@ -584,6 +585,38 @@ const validateLinkHeaderValue = hideStackFrames((hints) => { | |||
| 584 | 585 | ); | |
| 585 | 586 | }); | |
| 586 | 587 | ||
| 588 | + /** | ||
| 589 | + * Validates a single ignore option element (string, RegExp, or Function). | ||
| 590 | + * @param {*} value | ||
| 591 | + * @param {string} name | ||
| 592 | + */ | ||
| 593 | + const validateIgnoreOptionElement = hideStackFrames((value, name) => { | ||
| 594 | + if (typeof value === 'string') { | ||
| 595 | + if (value.length === 0) | ||
| 596 | + throw new ERR_INVALID_ARG_VALUE(name, value, 'must be a non-empty string'); | ||
| 597 | + return; | ||
| 598 | + } | ||
| 599 | + if (isRegExp(value)) return; | ||
| 600 | + if (typeof value === 'function') return; | ||
| 601 | + throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp', 'Function'], value); | ||
| 602 | + }); | ||
| 603 | + | ||
| 604 | + /** | ||
| 605 | + * Validates the ignore option for fs.watch. | ||
| 606 | + * @param {*} value | ||
| 607 | + * @param {string} name | ||
| 608 | + */ | ||
| 609 | + const validateIgnoreOption = hideStackFrames((value, name) => { | ||
| 610 | + if (value == null) return; | ||
| 611 | + if (ArrayIsArray(value)) { | ||
| 612 | + for (let i = 0; i < value.length; i++) { | ||
| 613 | + validateIgnoreOptionElement(value[i], `${name}[${i}]`); | ||
| 614 | + } | ||
| 615 | + return; | ||
| 616 | + } | ||
| 617 | + validateIgnoreOptionElement(value, name); | ||
| 618 | + }); | ||
| 619 | + | ||
| 587 | 620 | // 1. Returns false for undefined and NaN | |
| 588 | 621 | // 2. Returns true for finite numbers | |
| 589 | 622 | // 3. Throws ERR_INVALID_ARG_TYPE for non-numbers | |
@@ -637,6 +670,7 @@ module.exports = { | |||
| 637 | 670 | validateDictionary, | |
| 638 | 671 | validateEncoding, | |
| 639 | 672 | validateFunction, | |
| 673 | + validateIgnoreOption, | ||
| 640 | 674 | validateInt32, | |
| 641 | 675 | validateInteger, | |
| 642 | 676 | validateNumber, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments