diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 112f0b8be53..9c0eb1ed817 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -7,6 +7,7 @@ const { SafeMap, SafeSet, SafeWeakMap, + StringPrototypeEndsWith, StringPrototypeStartsWith, } = primordials; @@ -18,12 +19,19 @@ const EventEmitter = require('events'); const { addAbortListener } = require('internal/events/abort_listener'); const { watch } = require('fs'); const { fileURLToPath } = require('internal/url'); -const { resolve, dirname } = require('path'); +const { resolve, dirname, sep } = require('path'); const { setTimeout, clearTimeout } = require('timers'); const supportsRecursiveWatching = process.platform === 'win32' || process.platform === 'darwin'; +const isParentPath = (parentCandidate, childCandidate) => { + const parent = resolve(parentCandidate); + const child = resolve(childCandidate); + const normalizedParent = StringPrototypeEndsWith(parent, sep) ? parent : parent + sep; + return StringPrototypeStartsWith(child, normalizedParent); +}; + class FilesWatcher extends EventEmitter { #watchers = new SafeMap(); #filteredFiles = new SafeSet(); @@ -58,7 +66,7 @@ class FilesWatcher extends EventEmitter { } for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) { - if (watcher.recursive && StringPrototypeStartsWith(path, watchedPath)) { + if (watcher.recursive && isParentPath(watchedPath, path)) { return true; } } @@ -68,7 +76,7 @@ class FilesWatcher extends EventEmitter { #removeWatchedChildren(path) { for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) { - if (path !== watchedPath && StringPrototypeStartsWith(watchedPath, path)) { + if (path !== watchedPath && isParentPath(path, watchedPath)) { this.#unwatch(watcher); this.#watchers.delete(watchedPath); } diff --git a/test/parallel/test-watch-mode-files_watcher.mjs b/test/parallel/test-watch-mode-files_watcher.mjs index 2192a198d9c..e1595350cd0 100644 --- a/test/parallel/test-watch-mode-files_watcher.mjs +++ b/test/parallel/test-watch-mode-files_watcher.mjs @@ -6,7 +6,8 @@ import path from 'node:path'; import assert from 'node:assert'; import process from 'node:process'; import { describe, it, beforeEach, afterEach } from 'node:test'; -import { writeFileSync, mkdirSync } from 'node:fs'; +import { writeFileSync, mkdirSync, appendFileSync } from 'node:fs'; +import { createInterface } from 'node:readline'; import { setTimeout } from 'node:timers/promises'; import { once } from 'node:events'; import { spawn } from 'node:child_process'; @@ -51,6 +52,33 @@ describe('watch mode file watcher', () => { assert.strictEqual(changesCount, 1); }); + it('should watch changed files with same prefix path string', async () => { + mkdirSync(tmpdir.resolve('subdir')); + mkdirSync(tmpdir.resolve('sub')); + const file1 = tmpdir.resolve('subdir', 'file1.mjs'); + const file2 = tmpdir.resolve('sub', 'file2.mjs'); + writeFileSync(file2, 'export const hello = () => { return "hello world"; };'); + writeFileSync(file1, 'import { hello } from "../sub/file2.mjs"; console.log(hello());'); + + const child = spawn(process.execPath, + ['--watch', file1], + { stdio: ['ignore', 'pipe', 'ignore'] }); + let completeCount = 0; + for await (const line of createInterface(child.stdout)) { + if (!line.startsWith('Completed running')) { + continue; + } + completeCount++; + if (completeCount === 1) { + appendFileSync(file1, '\n // append 1'); + } + // The file is reloaded due to file watching + if (completeCount === 2) { + child.kill(); + } + } + }); + it('should debounce changes', async () => { const file = tmpdir.resolve('file2'); writeFileSync(file, 'written');