Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/vfs/watchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
19 changes: 19 additions & 0 deletions tests/fs/watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Loading