| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 75c06bc commit e91b296
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1837,6 +1837,10 @@ added: | |||
| 1837 | 1837 | * `overflow` {string} Either `'ignore'` or `'throw'` when there are more events to be | |
| 1838 | 1838 | queued than `maxQueue` allows. `'ignore'` means overflow events are dropped and a | |
| 1839 | 1839 | 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`. | ||
| 1840 | 1844 | * Returns: {AsyncIterator} of objects with the properties: | |
| 1841 | 1845 | * `eventType` {string} The type of change | |
| 1842 | 1846 | * `filename` {string|Buffer|null} The name of the file changed. | |
@@ -4804,6 +4808,10 @@ changes: | |||
| 4804 | 4808 | * `encoding` {string} Specifies the character encoding to be used for the | |
| 4805 | 4809 | filename passed to the listener. **Default:** `'utf8'`. | |
| 4806 | 4810 | * `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`. | ||
| 4807 | 4815 | * `listener` {Function|undefined} **Default:** `undefined` | |
| 4808 | 4816 | * `eventType` {string} | |
| 4809 | 4817 | * `filename` {string|Buffer|null} | |
@@ -8764,6 +8772,7 @@ the file contents. | |||
| 8764 | 8772 | [`fsPromises.utimes()`]: #fspromisesutimespath-atime-mtime | |
| 8765 | 8773 | [`inotify(7)`]: https://man7.org/linux/man-pages/man7/inotify.7.html | |
| 8766 | 8774 | [`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 | |
| 8775 | + [`minimatch`]: https://github.com/isaacs/minimatch | ||
| 8767 | 8776 | [`util.promisify()`]: util.md#utilpromisifyoriginal | |
| 8768 | 8777 | [bigints]: https://tc39.github.io/proposal-bigint | |
| 8769 | 8778 | [caveats]: #caveats | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2521,7 +2521,8 @@ function watch(filename, options, listener) { | |||
| 2521 | 2521 | watcher[watchers.kFSWatchStart](path, | |
| 2522 | 2522 | options.persistent, | |
| 2523 | 2523 | options.recursive, | |
| 2524 | - options.encoding); | ||
| 2524 | + options.encoding, | ||
| 2525 | + options.ignore); | ||
| 2525 | 2526 | } | |
| 2526 | 2527 | ||
| 2527 | 2528 | 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 | |
|---|---|---|---|
@@ -35,6 +35,7 @@ const { normalizeEncoding } = require('internal/util'); | |||
| 35 | 35 | const { | |
| 36 | 36 | isAsyncFunction, | |
| 37 | 37 | isArrayBufferView, | |
| 38 | + isRegExp, | ||
| 38 | 39 | } = require('internal/util/types'); | |
| 39 | 40 | const { signals } = internalBinding('constants').os; | |
| 40 | 41 | ||
@@ -575,6 +576,38 @@ const validateLinkHeaderValue = hideStackFrames((hints) => { | |||
| 575 | 576 | ); | |
| 576 | 577 | }); | |
| 577 | 578 | ||
| 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 | + | ||
| 578 | 611 | // 1. Returns false for undefined and NaN | |
| 579 | 612 | // 2. Returns true for finite numbers | |
| 580 | 613 | // 3. Throws ERR_INVALID_ARG_TYPE for non-numbers | |
@@ -628,6 +661,7 @@ module.exports = { | |||
| 628 | 661 | validateDictionary, | |
| 629 | 662 | validateEncoding, | |
| 630 | 663 | validateFunction, | |
| 664 | + validateIgnoreOption, | ||
| 631 | 665 | validateInt32, | |
| 632 | 666 | validateInteger, | |
| 633 | 667 | validateNumber, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments