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, []);