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');
+ });
});