From 0a288d36d6f46f22b8253e5b06d02bb2dfb5b9c3 Mon Sep 17 00:00:00 2001 From: RajeshKumar11 Date: Tue, 24 Feb 2026 22:47:32 +0530 Subject: [PATCH] watch: fix --watch-path restart on unrelated file with --env-file When both --watch-path= and --env-file= are provided, touching any file in the directory containing the env file incorrectly triggered a restart, even if the changed file was unrelated to the watched path. Root cause: filterFile() watches dirname(file) recursively on macOS and Windows (platforms with recursive fs.watch support). In 'filter' mode this is harmless because #onChange only fires for files in #filteredFiles. But in 'all' mode (used when --watch-path is set), #onChange fires for any change in any watched path without checking the filter, so watching the env file's parent directory caused restarts on every file change in that directory. Fix: only watch the parent directory in 'filter' mode. In 'all' mode, watch the specific file directly (watchPath(file, false)) so that changes to unrelated files in the same directory are not observed. Fixes: https://github.com/nodejs/node/issues/61906 --- lib/internal/watch_mode/files_watcher.js | 9 ++-- .../test-watch-mode-files_watcher.mjs | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 9c0eb1ed817c29..56a1540591758e 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -128,11 +128,14 @@ class FilesWatcher extends EventEmitter { filterFile(file, owner) { if (!file) return; - if (supportsRecursiveWatching) { + if (supportsRecursiveWatching && this.#mode === 'filter') { + // In filter mode, watch the parent directory with a single recursive + // FSWatcher - changes are then filtered by #filteredFiles in #onChange. this.watchPath(dirname(file)); } else { - // Having multiple FSWatcher's seems to be slower - // than a single recursive FSWatcher + // In 'all' mode, watch the specific file directly so that unrelated + // files in the same directory do not trigger unnecessary restarts. + // Also used on platforms without recursive watching support. this.watchPath(file, false); } this.#filteredFiles.add(file); diff --git a/test/parallel/test-watch-mode-files_watcher.mjs b/test/parallel/test-watch-mode-files_watcher.mjs index e1595350cd0f3e..f12a939b4124a9 100644 --- a/test/parallel/test-watch-mode-files_watcher.mjs +++ b/test/parallel/test-watch-mode-files_watcher.mjs @@ -161,6 +161,47 @@ describe('watch mode file watcher', () => { assert.strictEqual(changesCount, 1); }); + // Regression test for https://github.com/nodejs/node/issues/61906 + // When --watch-path is used (mode: 'all'), filterFile() is called for + // --env-file entries. It must watch only that specific file, not the + // entire parent directory, so that touching an unrelated file in the + // same directory does not trigger a restart. + it('filterFile in "all" mode should not trigger on unrelated files', + { skip: !supportsRecursiveWatching }, async () => { + watcher = new FilesWatcher({ debounce: 100, mode: 'all' }); + watcher.on('changed', common.mustNotCall( + 'unexpected restart triggered by unrelated file change')); + + const envFile = tmpdir.resolve('env-no-trigger.env'); + const unrelated = tmpdir.resolve('env-unrelated.txt'); + writeFileSync(envFile, 'FOO=bar'); + writeFileSync(unrelated, 'initial'); + + watcher.filterFile(envFile); + + await setTimeout(common.platformTimeout(100)); // avoid throttling + writeFileSync(unrelated, 'changed'); + // Wait long enough to confirm no restart was triggered + await setTimeout(1000); + }); + + it('filterFile in "all" mode should trigger when the watched file changes', + { skip: !supportsRecursiveWatching }, async () => { + watcher = new FilesWatcher({ debounce: 100, mode: 'all' }); + watcher.on('changed', () => changesCount++); + + const envFile = tmpdir.resolve('env-trigger.env'); + writeFileSync(envFile, 'FOO=bar'); + + watcher.filterFile(envFile); + + const changed = once(watcher, 'changed'); + await setTimeout(common.platformTimeout(100)); // avoid throttling + writeFileSync(envFile, 'FOO=newvalue'); + await changed; + assert.strictEqual(changesCount, 1); + }); + it('should ruse existing watcher if it exists', { skip: !supportsRecursiveWatching }, () => { assert.deepStrictEqual(watcher.watchedPaths, []);