From 0780548b9fef451b1bbd84641defbe9e2e0008d4 Mon Sep 17 00:00:00 2001 From: nzinfo Date: Wed, 17 Jun 2026 11:25:44 +0800 Subject: [PATCH] fix(watch): include '/' in emitChange parent walk --- src/vfs/watchers.ts | 15 +++++++++------ tests/fs/watch.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/vfs/watchers.ts b/src/vfs/watchers.ts index a978f71f..9f175a41 100644 --- a/src/vfs/watchers.ts +++ b/src/vfs/watchers.ts @@ -196,14 +196,17 @@ export function emitChange(context: V_Context, eventType: fs.WatchEventType, fil if ($) filename = join($.root ?? '/', filename); filename = normalizePath(filename); - // Notify watchers, including ones on parent directories if they are watching recursively - for (let path = filename; path != '/'; path = dirname(path)) { + // Notify watchers, including ones on parent directories if they are watching recursively. + // The walk includes '/' so a watcher on the root also fires (previously `path != '/'` skipped it). + for (let path = filename; ; path = dirname(path)) { const watchersForPath = watchers.get(path); - if (!watchersForPath) continue; - - for (const watcher of watchersForPath) { - watcher.emit('change', eventType, relative.call(watcher._context, path, filename) || basename(filename)); + if (watchersForPath) { + for (const watcher of watchersForPath) { + watcher.emit('change', eventType, relative.call(watcher._context, path, filename) || basename(filename)); + } } + + if (path === '/') break; } } diff --git a/tests/fs/watch.test.ts b/tests/fs/watch.test.ts index e1d6a7da..9ee5f2fe 100644 --- a/tests/fs/watch.test.ts +++ b/tests/fs/watch.test.ts @@ -156,4 +156,23 @@ suite('Watch', () => { await watcher.return!(); await promise; }); + + test('watch("/") receives events for files under root', async () => { + // Regression: emitChange previously exited its parent-walk before + // checking watchers.get('/'), so a watcher registered on '/' never fired. + const { promise, resolve } = Promise.withResolvers<[string, string]>(); + + using watcher = fs.watch('/', (eventType, filename) => { + resolve([eventType, filename]); + }); + + await fs.promises.writeFile('/watch-root-file.txt', 'x'); + + const [eventType, filename] = await Promise.race([ + promise, + new Promise<[string, string]>((_, reject) => setTimeout(() => reject(new Error('watch("/") timed out')), 1000)), + ]); + assert.equal(eventType, 'change'); + assert.equal(filename, 'watch-root-file.txt'); + }); });