Skip to content

Commit 0a288d3

Browse files
committed
watch: fix --watch-path restart on unrelated file with --env-file
When both --watch-path=<dir> and --env-file=<path> 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: #61906
1 parent 9145cc6 commit 0a288d3

File tree

2 files changed

+47
-3
lines changed

2 files changed

+47
-3
lines changed

lib/internal/watch_mode/files_watcher.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,14 @@ class FilesWatcher extends EventEmitter {
128128

129129
filterFile(file, owner) {
130130
if (!file) return;
131-
if (supportsRecursiveWatching) {
131+
if (supportsRecursiveWatching && this.#mode === 'filter') {
132+
// In filter mode, watch the parent directory with a single recursive
133+
// FSWatcher - changes are then filtered by #filteredFiles in #onChange.
132134
this.watchPath(dirname(file));
133135
} else {
134-
// Having multiple FSWatcher's seems to be slower
135-
// than a single recursive FSWatcher
136+
// In 'all' mode, watch the specific file directly so that unrelated
137+
// files in the same directory do not trigger unnecessary restarts.
138+
// Also used on platforms without recursive watching support.
136139
this.watchPath(file, false);
137140
}
138141
this.#filteredFiles.add(file);

test/parallel/test-watch-mode-files_watcher.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,47 @@ describe('watch mode file watcher', () => {
161161
assert.strictEqual(changesCount, 1);
162162
});
163163

164+
// Regression test for https://github.com/nodejs/node/issues/61906
165+
// When --watch-path is used (mode: 'all'), filterFile() is called for
166+
// --env-file entries. It must watch only that specific file, not the
167+
// entire parent directory, so that touching an unrelated file in the
168+
// same directory does not trigger a restart.
169+
it('filterFile in "all" mode should not trigger on unrelated files',
170+
{ skip: !supportsRecursiveWatching }, async () => {
171+
watcher = new FilesWatcher({ debounce: 100, mode: 'all' });
172+
watcher.on('changed', common.mustNotCall(
173+
'unexpected restart triggered by unrelated file change'));
174+
175+
const envFile = tmpdir.resolve('env-no-trigger.env');
176+
const unrelated = tmpdir.resolve('env-unrelated.txt');
177+
writeFileSync(envFile, 'FOO=bar');
178+
writeFileSync(unrelated, 'initial');
179+
180+
watcher.filterFile(envFile);
181+
182+
await setTimeout(common.platformTimeout(100)); // avoid throttling
183+
writeFileSync(unrelated, 'changed');
184+
// Wait long enough to confirm no restart was triggered
185+
await setTimeout(1000);
186+
});
187+
188+
it('filterFile in "all" mode should trigger when the watched file changes',
189+
{ skip: !supportsRecursiveWatching }, async () => {
190+
watcher = new FilesWatcher({ debounce: 100, mode: 'all' });
191+
watcher.on('changed', () => changesCount++);
192+
193+
const envFile = tmpdir.resolve('env-trigger.env');
194+
writeFileSync(envFile, 'FOO=bar');
195+
196+
watcher.filterFile(envFile);
197+
198+
const changed = once(watcher, 'changed');
199+
await setTimeout(common.platformTimeout(100)); // avoid throttling
200+
writeFileSync(envFile, 'FOO=newvalue');
201+
await changed;
202+
assert.strictEqual(changesCount, 1);
203+
});
204+
164205
it('should ruse existing watcher if it exists',
165206
{ skip: !supportsRecursiveWatching }, () => {
166207
assert.deepStrictEqual(watcher.watchedPaths, []);

0 commit comments

Comments
 (0)