Skip to content

Commit 0d7e61f

Browse files
committed
fix: handle non-regular files in dev watcher to prevent crashes
The dev file watcher crashes when Unix socket files (e.g. from overmind or pm2) exist in the project root. chokidar v4 calls fs.watch() on these files, which fails on macOS with errno -102 (UNKNOWN). - Extract watcher ignore logic into an exported `isIgnoredByWatcher` function for testability - Run cheap path-based checks before the fs.statSync call to avoid unnecessary syscalls on already-ignored paths - Filter non-regular files (sockets, pipes, etc.) via fs.statSync - Add an error handler on the watcher so unexpected errors log a warning instead of crashing the process Fixes: https://github.com/RomanBaiocco/react-router-sock-repro Related: paulmillr/chokidar#1391
1 parent c5012af commit 0d7e61f

File tree

3 files changed

+112
-11
lines changed

3 files changed

+112
-11
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
Fix `react-router dev` crash when Unix socket files exist in the project root
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import path from "node:path";
2+
import fs from "node:fs";
3+
import net from "node:net";
4+
import os from "node:os";
5+
6+
import { isIgnoredByWatcher } from "../config/config";
7+
8+
describe("isIgnoredByWatcher", () => {
9+
let tmpDir: string;
10+
let root: string;
11+
let appDirectory: string;
12+
13+
beforeEach(() => {
14+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "rr-watcher-test-"));
15+
root = tmpDir;
16+
appDirectory = path.join(root, "app");
17+
fs.mkdirSync(appDirectory, { recursive: true });
18+
});
19+
20+
afterEach(() => {
21+
fs.rmSync(tmpDir, { recursive: true, force: true });
22+
});
23+
24+
it("does not ignore regular files at root level", () => {
25+
let filePath = path.join(root, "react-router.config.ts");
26+
fs.writeFileSync(filePath, "");
27+
expect(isIgnoredByWatcher(filePath, { root, appDirectory })).toBe(false);
28+
});
29+
30+
it("does not ignore the root directory itself", () => {
31+
expect(isIgnoredByWatcher(root, { root, appDirectory })).toBe(false);
32+
});
33+
34+
it("does not ignore files inside app directory", () => {
35+
let filePath = path.join(appDirectory, "root.tsx");
36+
fs.writeFileSync(filePath, "");
37+
expect(isIgnoredByWatcher(filePath, { root, appDirectory })).toBe(false);
38+
});
39+
40+
it("ignores files in subdirectories outside the app directory", () => {
41+
let subDir = path.join(root, "node_modules", "some-package");
42+
fs.mkdirSync(subDir, { recursive: true });
43+
let filePath = path.join(subDir, "index.js");
44+
fs.writeFileSync(filePath, "");
45+
expect(isIgnoredByWatcher(filePath, { root, appDirectory })).toBe(true);
46+
});
47+
48+
it("ignores Unix socket files at the root level", (done) => {
49+
let socketPath = path.join(root, "overmind.sock");
50+
let server = net.createServer();
51+
52+
server.listen(socketPath, () => {
53+
expect(isIgnoredByWatcher(socketPath, { root, appDirectory })).toBe(
54+
true,
55+
);
56+
server.close(done);
57+
});
58+
});
59+
60+
it("ignores paths that cannot be stat'd", () => {
61+
let nonexistent = path.join(root, "ghost.sock");
62+
// File doesn't exist — statSync with throwIfNoEntry: false returns
63+
// undefined, so this should fall through to `return false`. But if
64+
// statSync throws for another reason, the catch block returns true.
65+
// Here we just verify it doesn't throw.
66+
expect(() =>
67+
isIgnoredByWatcher(nonexistent, { root, appDirectory }),
68+
).not.toThrow();
69+
});
70+
});

packages/react-router-dev/config/config.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -818,17 +818,14 @@ export async function createConfigLoader({
818818
if (!fsWatcher) {
819819
fsWatcher = chokidar.watch([root, appDirectory], {
820820
ignoreInitial: true,
821-
ignored: (path) => {
822-
let dirname = Path.dirname(path);
823-
824-
return (
825-
!dirname.startsWith(appDirectory) &&
826-
// Ensure we're only watching files outside of the app directory
827-
// that are at the root level, not nested in subdirectories
828-
path !== root && // Watch the root directory itself
829-
dirname !== root // Watch files at the root level
830-
);
831-
},
821+
ignored: (path) =>
822+
isIgnoredByWatcher(path, { root, appDirectory }),
823+
});
824+
825+
fsWatcher.on("error", (error: unknown) => {
826+
let message =
827+
error instanceof Error ? error.message : String(error);
828+
console.warn(colors.yellow(`File watcher error: ${message}`));
832829
});
833830

834831
fsWatcher.on("all", async (...args) => {
@@ -1166,3 +1163,32 @@ function isEntryFileDependency(
11661163

11671164
return false;
11681165
}
1166+
1167+
export function isIgnoredByWatcher(
1168+
path: string,
1169+
{ root, appDirectory }: { root: string; appDirectory: string },
1170+
): boolean {
1171+
let dirname = Path.dirname(path);
1172+
1173+
let ignoredByPath =
1174+
!dirname.startsWith(appDirectory) &&
1175+
path !== root &&
1176+
dirname !== root;
1177+
1178+
if (ignoredByPath) {
1179+
return true;
1180+
}
1181+
1182+
// Filter out non-regular files (sockets, pipes, etc.) that
1183+
// crash fs.watch() on macOS with errno -102
1184+
try {
1185+
let stat = fs.statSync(path, { throwIfNoEntry: false });
1186+
if (stat && !stat.isFile() && !stat.isDirectory()) {
1187+
return true;
1188+
}
1189+
} catch {
1190+
return true;
1191+
}
1192+
1193+
return false;
1194+
}

0 commit comments

Comments
 (0)