From c3cc140ee050eeca6e154f7a01676e1f41bb97f9 Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Sat, 19 Sep 2026 15:54:42 -0300 Subject: [PATCH] watch: track all dependencies of an owner filterFile() reads #ownerDependencies with the dependency path, but writes the set back keyed by the owner, so each call starts a fresh set and overwrites the previous one. An owner keeps only the last dependency registered for it. unfilterFilesOwnedBy() walks that set to release an owner's dependencies, so every other one stays in #filteredFiles and #dependencyOwners. Under `node --test --watch` a restart re-reports the modules the test still loads, which hides it for those, but a dependency it stopped requiring is never reported again and keeps re-running the test. Read #ownerDependencies with the key it is written with, and collapse the repeated #dependencyOwners lookup in the loop, which now runs once per dependency rather than once per owner. Signed-off-by: Christian Aurich Zanettini Martins --- lib/internal/watch_mode/files_watcher.js | 11 ++++++---- .../test-watch-mode-files_watcher.mjs | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index c87f489b6377..aea2e33d9399 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -143,7 +143,7 @@ class FilesWatcher extends EventEmitter { this.#filteredFiles.add(file); if (owner) { const owners = this.#dependencyOwners.get(file) ?? new SafeSet(); - const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet(); + const dependencies = this.#ownerDependencies.get(owner) ?? new SafeSet(); owners.add(owner); dependencies.add(file); this.#dependencyOwners.set(file, owners); @@ -193,9 +193,12 @@ class FilesWatcher extends EventEmitter { owners.forEach((owner) => { this.#ownerDependencies.get(owner)?.forEach((dependency) => { this.#filteredFiles.delete(dependency); - this.#dependencyOwners.get(dependency)?.delete(owner); - if (this.#dependencyOwners.get(dependency)?.size === 0) { - this.#dependencyOwners.delete(dependency); + const dependencyOwners = this.#dependencyOwners.get(dependency); + if (dependencyOwners !== undefined) { + dependencyOwners.delete(owner); + if (dependencyOwners.size === 0) { + this.#dependencyOwners.delete(dependency); + } } }); this.#filteredFiles.delete(owner); diff --git a/test/parallel/test-watch-mode-files_watcher.mjs b/test/parallel/test-watch-mode-files_watcher.mjs index e1595350cd0f..ddc5a13caa70 100644 --- a/test/parallel/test-watch-mode-files_watcher.mjs +++ b/test/parallel/test-watch-mode-files_watcher.mjs @@ -146,6 +146,26 @@ describe('watch mode file watcher', () => { assert.strictEqual(changesCount, 1); }); + it('should unfilter every dependency of an unfiltered owner', async () => { + const owner = tmpdir.resolve('owner.js'); + const dependency1 = tmpdir.resolve('owner-dependency-1.js'); + const dependency2 = tmpdir.resolve('owner-dependency-2.js'); + for (const file of [owner, dependency1, dependency2]) { + writeFileSync(file, 'written'); + } + + watcher.filterFile(owner); + watcher.filterFile(dependency1, owner); + watcher.filterFile(dependency2, owner); + watcher.unfilterFilesOwnedBy([owner]); + + watcher.on('changed', common.mustNotCall()); + writeFileSync(dependency1, '1'); + writeFileSync(dependency2, '2'); + // Wait for this long to make sure changes are not triggered + await setTimeout(1000); + }); + it('should watch all files in watched path when in "all" mode', { skip: !supportsRecursiveWatching }, async () => { watcher = new FilesWatcher({ debounce: 100, mode: 'all' });