Skip to content

Commit b769b7b

Browse files
committed
Fix DevTools port conflict during type generation
Prevents EADDRINUSE error when running typegen and dev server simultaneously by detecting type generation context and disabling DevTools. - Add isTypegenContext() to detect typegen via env vars and argv - Return empty plugin array when in typegen context - Add comprehensive unit tests (18 tests total) - Maintain backward compatibility for normal dev mode Fixes port 42069 conflict with TanStack DevTools during typegen execution.
1 parent 812c695 commit b769b7b

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest"
2+
import { reactRouterDevTools } from "./plugin"
3+
4+
// Mock the detect-typegen module
5+
vi.mock("./utils/detect-typegen", () => ({
6+
isTypegenContext: vi.fn(),
7+
}))
8+
9+
describe("reactRouterDevTools", () => {
10+
let isTypegenContext: ReturnType<typeof vi.fn>
11+
12+
beforeEach(async () => {
13+
vi.clearAllMocks()
14+
const module = await import("./utils/detect-typegen")
15+
isTypegenContext = vi.mocked(module.isTypegenContext)
16+
})
17+
18+
describe("typegen context", () => {
19+
it("should return empty array when in typegen context", () => {
20+
isTypegenContext.mockReturnValue(true)
21+
22+
const plugins = reactRouterDevTools()
23+
24+
expect(plugins).toEqual([])
25+
expect(isTypegenContext).toHaveBeenCalledTimes(1)
26+
})
27+
28+
it("should return empty array even with config options in typegen context", () => {
29+
isTypegenContext.mockReturnValue(true)
30+
31+
const plugins = reactRouterDevTools({
32+
client: { expansionLevel: 2 },
33+
})
34+
35+
expect(plugins).toEqual([])
36+
expect(isTypegenContext).toHaveBeenCalledTimes(1)
37+
})
38+
})
39+
40+
describe("normal context", () => {
41+
it("should return plugin array in normal context", () => {
42+
isTypegenContext.mockReturnValue(false)
43+
44+
const plugins = reactRouterDevTools()
45+
46+
expect(Array.isArray(plugins)).toBe(true)
47+
expect(plugins.length).toBeGreaterThan(0)
48+
expect(isTypegenContext).toHaveBeenCalledTimes(1)
49+
})
50+
51+
it("should return multiple plugins including TanStack DevTools", () => {
52+
isTypegenContext.mockReturnValue(false)
53+
54+
const plugins = reactRouterDevTools()
55+
56+
// TanStack DevTools returns an array that gets spread
57+
// + 5 custom plugins = 6+ total
58+
expect(plugins.length).toBeGreaterThan(5)
59+
60+
// Verify plugin names exist
61+
const pluginNames = plugins.filter((p) => p.name).map((p) => p.name)
62+
63+
expect(pluginNames).toContain("react-router-devtools")
64+
})
65+
66+
it("should return plugin array when config options are provided", () => {
67+
isTypegenContext.mockReturnValue(false)
68+
69+
const plugins = reactRouterDevTools({
70+
server: { silent: false },
71+
})
72+
73+
expect(Array.isArray(plugins)).toBe(true)
74+
expect(plugins.length).toBeGreaterThan(5)
75+
})
76+
})
77+
})

packages/react-router-devtools/src/vite/plugin.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { augmentDataFetchingFunctions } from "./utils/data-functions-augment.js"
1111
import { injectRdtClient } from "./utils/inject-client.js"
1212
import { injectContext } from "./utils/inject-context.js"
1313
import { augmentMiddlewareFunctions } from "./utils/middleware-augment.js"
14+
import { isTypegenContext } from "./utils/detect-typegen.js"
1415
// this should mirror the types in server/config.ts as well as they are bundled separately.
1516
declare global {
1617
interface Window {
@@ -170,6 +171,11 @@ type Route = {
170171
export const defineRdtConfig = (config: ReactRouterViteConfig) => config
171172

172173
export const reactRouterDevTools: (args?: ReactRouterViteConfig) => Plugin[] = (args) => {
174+
// Return empty array in typegen context (disable DevTools)
175+
if (isTypegenContext()) {
176+
return []
177+
}
178+
173179
const serverConfig = args?.server || {}
174180
const clientConfig = {
175181
...args?.client,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
2+
import { isTypegenContext } from "./detect-typegen"
3+
4+
describe("isTypegenContext", () => {
5+
const originalEnv = process.env
6+
const originalArgv = process.argv
7+
8+
beforeEach(() => {
9+
// Reset environment to clean state
10+
process.env = { ...originalEnv }
11+
process.argv = [...originalArgv]
12+
})
13+
14+
afterEach(() => {
15+
// Restore environment after test
16+
process.env = originalEnv
17+
process.argv = originalArgv
18+
})
19+
20+
describe("environment variable detection", () => {
21+
it("should return true when TYPEGEN_RUNNING=1", () => {
22+
process.env.TYPEGEN_RUNNING = "1"
23+
expect(isTypegenContext()).toBe(true)
24+
})
25+
26+
it("should return true when SAFE_ROUTES_TYPEGEN=1", () => {
27+
process.env.SAFE_ROUTES_TYPEGEN = "1"
28+
expect(isTypegenContext()).toBe(true)
29+
})
30+
31+
it("should return true when npm_lifecycle_event contains typegen (pre)", () => {
32+
// cspell:disable-next-line
33+
process.env.npm_lifecycle_event = "pretypegen"
34+
expect(isTypegenContext()).toBe(true)
35+
})
36+
37+
it("should return true when npm_lifecycle_event contains typegen (post)", () => {
38+
// cspell:disable-next-line
39+
process.env.npm_lifecycle_event = "posttypegen"
40+
expect(isTypegenContext()).toBe(true)
41+
})
42+
})
43+
44+
describe("command line argument detection", () => {
45+
it("should return true when process.argv contains typegen", () => {
46+
process.argv = ["node", "script.js", "typegen"]
47+
expect(isTypegenContext()).toBe(true)
48+
})
49+
50+
it("should return true when process.argv contains type-gen", () => {
51+
process.argv = ["node", "script.js", "type-gen"]
52+
expect(isTypegenContext()).toBe(true)
53+
})
54+
55+
it("should return true when process.argv contains react-router typegen", () => {
56+
process.argv = ["node", "script.js", "react-router", "typegen"]
57+
expect(isTypegenContext()).toBe(true)
58+
})
59+
60+
it("should return true when process.argv contains safe-routes", () => {
61+
process.argv = ["node", "script.js", "safe-routes"]
62+
expect(isTypegenContext()).toBe(true)
63+
})
64+
})
65+
66+
describe("normal context", () => {
67+
it("should return false when no typegen indicators are present", () => {
68+
expect(isTypegenContext()).toBe(false)
69+
})
70+
71+
it("should return false with unrelated environment variables", () => {
72+
process.env.OTHER_VAR = "1"
73+
expect(isTypegenContext()).toBe(false)
74+
})
75+
76+
it("should return false with unrelated command line arguments", () => {
77+
process.argv = ["node", "script.js", "dev", "build"]
78+
expect(isTypegenContext()).toBe(false)
79+
})
80+
81+
it("should return false when TYPEGEN_RUNNING=0", () => {
82+
process.env.TYPEGEN_RUNNING = "0"
83+
expect(isTypegenContext()).toBe(false)
84+
})
85+
86+
it("should return false when TYPEGEN_RUNNING is empty string", () => {
87+
process.env.TYPEGEN_RUNNING = ""
88+
expect(isTypegenContext()).toBe(false)
89+
})
90+
})
91+
})
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Detects if running in a typegen context
3+
*
4+
* Returns true if any of the following conditions are met:
5+
* - Environment variables: TYPEGEN_RUNNING=1, SAFE_ROUTES_TYPEGEN=1
6+
* - npm script: npm_lifecycle_event contains "typegen"
7+
* - Command line arguments: contains "typegen", "type-gen", "react-router typegen", or "safe-routes"
8+
*
9+
* @returns true if in typegen context, false otherwise
10+
*/
11+
export function isTypegenContext(): boolean {
12+
// Check environment variables
13+
if (
14+
process.env.TYPEGEN_RUNNING === "1" ||
15+
process.env.SAFE_ROUTES_TYPEGEN === "1"
16+
) {
17+
return true
18+
}
19+
20+
// Check npm lifecycle event
21+
if (process.env.npm_lifecycle_event?.includes("typegen")) {
22+
return true
23+
}
24+
25+
// Check command line arguments
26+
const args = process.argv.join(" ")
27+
if (
28+
args.includes("typegen") ||
29+
args.includes("type-gen") ||
30+
args.includes("react-router typegen") ||
31+
args.includes("safe-routes")
32+
) {
33+
return true
34+
}
35+
36+
return false
37+
}

0 commit comments

Comments
 (0)