@react-router/dev watcher has no exclusion mechanism — chokidar 4 holds an fd per watched file on macOS; large trees under appDirectory break all child_process spawning (spawn EBADF)
Summary
@react-router/dev's dev watcher (chokidar.watch([root, appDirectory], { ignored: isIgnoredByWatcher }) — packages/react-router-dev/config/config.ts#L1231) watches everything under appDirectory with a hardcoded ignore function that exposes no user configuration and does not honor Vite's server.watch.ignored.
Since chokidar 4 removed fsevents, macOS watching falls back to kqueue-style watching — one open file descriptor per watched file. A large tree inside appDirectory (in our case a vendored 20,803-file dbt/Python project at app/lib/insights) puts the dev-server process at a ~24k-fd table, at which point every child_process spawn in the process fails with spawn EBADF (errno: -9, syscall: spawn) — for us, surfacing through a Vite plugin that shells out to cargo, and misattributed to that plugin for weeks.
Proof of the fd → EBADF mechanism (clean-room)
// node 25.8.1 & 24.17.0, macOS (darwin 25.5, arm64)
const fs = require('fs'), { execFile } = require('child_process');
for (let i = 0; i < 24500; i++) fs.openSync('/dev/null', 'r');
execFile('cargo', ['--version'], (e, o) => console.log(e ? e.code : o)); // → Error: spawn EBADF
Reproduction (React Router)
- RRv7 app (framework mode). Create a big tree inside the app dir:
mkdir -p app/big && cd app/big && for i in $(seq 1 25000); do touch f$i.txt; done - Add any loader that spawns:
execFile('echo', ['hi']). react-router dev, request the route →spawn EBADF.lsof -p <dev pid> | wc -lshows ~25k fds, one per file inapp/big.- Adding
server.watch.ignored: ['**/app/big/**']to the Vite config changes nothing (measured: fd count identical) because this watcher is separate from Vite's.
System info
- macOS (darwin 25.5, arm64), Node 24.17.0 / 25.8.1, npm
- react-router / @react-router/dev 7.18.1 (code unchanged on
main) - chokidar 4.0.3 (via @react-router/dev)
Expected
Some way to exclude directories from the dev watcher: a config option (e.g. watchIgnore / dev.watchExclusions), or honoring Vite's server.watch.ignored, or both. (Longer-term: chokidar-4's fsevents removal makes per-file fds the macOS default — worth considering @parcel/watcher or fsevents for large-tree friendliness.)
Actual
isIgnoredByWatcher is hardcoded; anything under appDirectory is watched unconditionally, and sufficiently large trees brick child-process spawning for the whole dev server with an error that points nowhere near the cause.
Full forensic write-up (fd captures, threshold measurements ~24k, the misattribution trail): https://github.com/kadeangell/vite-plugin-native-rust/issues/6
Source: remix-run/react-router