diff --git a/packages/react-router-devtools/src/vite/plugin.test.ts b/packages/react-router-devtools/src/vite/plugin.test.ts new file mode 100644 index 0000000..875d6f7 --- /dev/null +++ b/packages/react-router-devtools/src/vite/plugin.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" +import { reactRouterDevTools } from "./plugin" + +// Mock the detect-typegen module +vi.mock("./utils/detect-typegen", () => ({ + isTypegenContext: vi.fn(), +})) + +describe("reactRouterDevTools", () => { + let isTypegenContext: ReturnType + + beforeEach(async () => { + vi.clearAllMocks() + const module = await import("./utils/detect-typegen") + isTypegenContext = vi.mocked(module.isTypegenContext) + }) + + describe("typegen context", () => { + it("should return empty array when in typegen context", () => { + isTypegenContext.mockReturnValue(true) + + const plugins = reactRouterDevTools() + + expect(plugins).toEqual([]) + expect(isTypegenContext).toHaveBeenCalledTimes(1) + }) + + it("should return empty array even with config options in typegen context", () => { + isTypegenContext.mockReturnValue(true) + + const plugins = reactRouterDevTools({ + client: { expansionLevel: 2 }, + }) + + expect(plugins).toEqual([]) + expect(isTypegenContext).toHaveBeenCalledTimes(1) + }) + }) + + describe("normal context", () => { + it("should return plugin array in normal context", () => { + isTypegenContext.mockReturnValue(false) + + const plugins = reactRouterDevTools() + + expect(Array.isArray(plugins)).toBe(true) + expect(plugins.length).toBeGreaterThan(0) + expect(isTypegenContext).toHaveBeenCalledTimes(1) + }) + + it("should return multiple plugins including TanStack DevTools", () => { + isTypegenContext.mockReturnValue(false) + + const plugins = reactRouterDevTools() + + // TanStack DevTools returns an array that gets spread + // + 5 custom plugins = 6+ total + expect(plugins.length).toBeGreaterThan(5) + + // Verify plugin names exist + const pluginNames = plugins.filter((p) => p.name).map((p) => p.name) + + expect(pluginNames).toContain("react-router-devtools") + }) + + it("should return plugin array when config options are provided", () => { + isTypegenContext.mockReturnValue(false) + + const plugins = reactRouterDevTools({ + server: { silent: false }, + }) + + expect(Array.isArray(plugins)).toBe(true) + expect(plugins.length).toBeGreaterThan(5) + }) + }) +}) diff --git a/packages/react-router-devtools/src/vite/plugin.tsx b/packages/react-router-devtools/src/vite/plugin.tsx index 2377296..2b7dd8f 100644 --- a/packages/react-router-devtools/src/vite/plugin.tsx +++ b/packages/react-router-devtools/src/vite/plugin.tsx @@ -11,6 +11,7 @@ import { augmentDataFetchingFunctions } from "./utils/data-functions-augment.js" import { injectRdtClient } from "./utils/inject-client.js" import { injectContext } from "./utils/inject-context.js" import { augmentMiddlewareFunctions } from "./utils/middleware-augment.js" +import { isTypegenContext } from "./utils/detect-typegen.js" // this should mirror the types in server/config.ts as well as they are bundled separately. declare global { interface Window { @@ -170,6 +171,11 @@ type Route = { export const defineRdtConfig = (config: ReactRouterViteConfig) => config export const reactRouterDevTools: (args?: ReactRouterViteConfig) => Plugin[] = (args) => { + // Return empty array in typegen context (disable DevTools) + if (isTypegenContext()) { + return [] + } + const serverConfig = args?.server || {} const clientConfig = { ...args?.client, diff --git a/packages/react-router-devtools/src/vite/utils/detect-typegen.test.ts b/packages/react-router-devtools/src/vite/utils/detect-typegen.test.ts new file mode 100644 index 0000000..0f7ba5f --- /dev/null +++ b/packages/react-router-devtools/src/vite/utils/detect-typegen.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest" +import { isTypegenContext } from "./detect-typegen" + +describe("isTypegenContext", () => { + const originalEnv = process.env + const originalArgv = process.argv + + beforeEach(() => { + // Reset environment to clean state + process.env = { ...originalEnv } + process.argv = [...originalArgv] + }) + + afterEach(() => { + // Restore environment after test + process.env = originalEnv + process.argv = originalArgv + }) + + describe("environment variable detection", () => { + it("should return true when TYPEGEN_RUNNING=1", () => { + process.env.TYPEGEN_RUNNING = "1" + expect(isTypegenContext()).toBe(true) + }) + + it("should return true when SAFE_ROUTES_TYPEGEN=1", () => { + process.env.SAFE_ROUTES_TYPEGEN = "1" + expect(isTypegenContext()).toBe(true) + }) + + it("should return true when npm_lifecycle_event contains typegen (pre)", () => { + // cspell:disable-next-line + process.env.npm_lifecycle_event = "pretypegen" + expect(isTypegenContext()).toBe(true) + }) + + it("should return true when npm_lifecycle_event contains typegen (post)", () => { + // cspell:disable-next-line + process.env.npm_lifecycle_event = "posttypegen" + expect(isTypegenContext()).toBe(true) + }) + }) + + describe("command line argument detection", () => { + it("should return true when process.argv contains typegen", () => { + process.argv = ["node", "script.js", "typegen"] + expect(isTypegenContext()).toBe(true) + }) + + it("should return true when process.argv contains type-gen", () => { + process.argv = ["node", "script.js", "type-gen"] + expect(isTypegenContext()).toBe(true) + }) + + it("should return true when process.argv contains react-router typegen", () => { + process.argv = ["node", "script.js", "react-router", "typegen"] + expect(isTypegenContext()).toBe(true) + }) + + it("should return true when process.argv contains safe-routes", () => { + process.argv = ["node", "script.js", "safe-routes"] + expect(isTypegenContext()).toBe(true) + }) + }) + + describe("normal context", () => { + it("should return false when no typegen indicators are present", () => { + expect(isTypegenContext()).toBe(false) + }) + + it("should return false with unrelated environment variables", () => { + process.env.OTHER_VAR = "1" + expect(isTypegenContext()).toBe(false) + }) + + it("should return false with unrelated command line arguments", () => { + process.argv = ["node", "script.js", "dev", "build"] + expect(isTypegenContext()).toBe(false) + }) + + it("should return false when TYPEGEN_RUNNING=0", () => { + process.env.TYPEGEN_RUNNING = "0" + expect(isTypegenContext()).toBe(false) + }) + + it("should return false when TYPEGEN_RUNNING is empty string", () => { + process.env.TYPEGEN_RUNNING = "" + expect(isTypegenContext()).toBe(false) + }) + }) +}) diff --git a/packages/react-router-devtools/src/vite/utils/detect-typegen.ts b/packages/react-router-devtools/src/vite/utils/detect-typegen.ts new file mode 100644 index 0000000..8f7eaca --- /dev/null +++ b/packages/react-router-devtools/src/vite/utils/detect-typegen.ts @@ -0,0 +1,37 @@ +/** + * Detects if running in a typegen context + * + * Returns true if any of the following conditions are met: + * - Environment variables: TYPEGEN_RUNNING=1, SAFE_ROUTES_TYPEGEN=1 + * - npm script: npm_lifecycle_event contains "typegen" + * - Command line arguments: contains "typegen", "type-gen", "react-router typegen", or "safe-routes" + * + * @returns true if in typegen context, false otherwise + */ +export function isTypegenContext(): boolean { + // Check environment variables + if ( + process.env.TYPEGEN_RUNNING === "1" || + process.env.SAFE_ROUTES_TYPEGEN === "1" + ) { + return true + } + + // Check npm lifecycle event + if (process.env.npm_lifecycle_event?.includes("typegen")) { + return true + } + + // Check command line arguments + const args = process.argv.join(" ") + if ( + args.includes("typegen") || + args.includes("type-gen") || + args.includes("react-router typegen") || + args.includes("safe-routes") + ) { + return true + } + + return false +}