Skip to content

Commit 8470938

Browse files
fix(vite): cover the fail-open fallback on Linux before Node 20
The vite plugin's fail-open watcher -- engaged only when Retrigger degrades mid-session -- used fs.watch(root, { recursive: true }), which does not exist on Linux until Node 20. On node18-linux the recursive attach threw, the catch left the tree uncovered, and a post-degradation edit never reached server.watcher, so the CI matrix's node18-linux leg failed ("keeps delivering events through a fallback watcher after mid-session degradation" timed out) while node20/22 and macOS/Windows (native recursive) passed. When recursive fs.watch is unavailable, fall back to one non-recursive handle per directory, extended to subdirectories as they appear -- the same shape the JS engine uses where the platform has no recursive watch. Verified both the per-directory path (delivers the event) and the recursive path (unregressed).
1 parent 8828764 commit 8470938

1 file changed

Lines changed: 43 additions & 4 deletions

File tree

src/bindings/nodejs/plugins/vite-plugin.js

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,18 @@ const ERROR_STREAK_LIMIT = 5;
8282
* neither `vite`'s own `FSWatcher` nor `chokidar` are usable here.
8383
*
8484
* `fs.watch(dir, { recursive: true })` is natively supported on macOS and Windows, and by Node
85-
* itself on Linux since v20.13; where a root does not support it, or has vanished, that one root
86-
* is silently left uncovered rather than throwing — this watcher's whole purpose is to never be
87-
* the reason a dev server hook throws.
85+
* itself on Linux since v20; where recursion is unavailable (Linux before Node 20) it falls back to
86+
* one non-recursive handle per directory, extended as subdirectories appear, so the tree still stays
87+
* covered. A root that has vanished is skipped rather than throwing — this watcher's whole purpose
88+
* is to never be the reason a dev server hook throws.
8889
*/
8990
class FailOpenWatcher extends EventEmitter {
9091
constructor(roots) {
9192
super();
9293
/** @type {import('fs').FSWatcher[]} */
9394
this._handles = [];
95+
/** @type {Set<string>} directories already covered by a non-recursive handle. */
96+
this._watchedDirs = new Set();
9497
for (const root of roots) this._attach(root);
9598
}
9699

@@ -104,10 +107,45 @@ class FailOpenWatcher extends EventEmitter {
104107
});
105108
this._handles.push(handle);
106109
} catch {
107-
/* recursive fs.watch unsupported here, or the root disappeared before this could attach */
110+
// Recursive fs.watch is unavailable here -- Linux before Node 20 -- or the root vanished
111+
// mid-attach. Rather than leave the tree silently uncovered, watch every directory
112+
// non-recursively and extend to new subdirectories as they appear: the same shape the JS
113+
// engine uses where the platform has no recursive watch.
114+
this._attachByDirectory(root);
108115
}
109116
}
110117

118+
/** One handle per directory, extended to subdirectories as they are created, for platforms
119+
* without recursive fs.watch. */
120+
_attachByDirectory(dir) {
121+
if (this._watchedDirs.has(dir)) return;
122+
let handle;
123+
try {
124+
handle = fs.watch(dir, { persistent: false }, (_type, filename) => {
125+
if (!filename) return;
126+
const target = path.join(dir, filename.toString());
127+
fs.stat(target, (err, stat) => {
128+
if (err) this.emit('unlink', target);
129+
else if (stat.isDirectory()) this._attachByDirectory(target);
130+
else this.emit('change', target);
131+
});
132+
});
133+
} catch {
134+
return; /* the directory disappeared before this could attach */
135+
}
136+
handle.on('error', () => {
137+
/* one directory's handle failing must not take the others down */
138+
});
139+
this._watchedDirs.add(dir);
140+
this._handles.push(handle);
141+
fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
142+
if (err) return;
143+
for (const entry of entries) {
144+
if (entry.isDirectory()) this._attachByDirectory(path.join(dir, entry.name));
145+
}
146+
});
147+
}
148+
111149
/** Neither a size nor a rename direction is available from a raw `fs.watch` event, so a path
112150
* that still exists is reported as `change` (harmless for one Vite does not know about, and
113151
* enough to invalidate one it does) and a path that does not is reported as `unlink`. */
@@ -135,6 +173,7 @@ class FailOpenWatcher extends EventEmitter {
135173
}
136174
}
137175
this._handles = [];
176+
this._watchedDirs.clear();
138177
}
139178
}
140179

0 commit comments

Comments
 (0)