Skip to content

Commit 5c7f952

Browse files
committed
Fix virtual module type to properly handle optional properties
The virtual module `virtual:react-router/server-build` now deliberately implements CommonJS `export =` syntax to correctly support optional properties in the `ServerBuild` type, ensuring proper TypeScript compatibility with `exactOptionalPropertyTypes`. Signed-off-by: Sora Morimoto <[email protected]>
1 parent c0f766f commit 5c7f952

File tree

3 files changed

+45
-12
lines changed

3 files changed

+45
-12
lines changed

.changeset/tiny-gifts-wash.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
Fix virtual module type to properly handle optional properties.
6+
7+
The virtual module `virtual:react-router/server-build` now deliberately implements CommonJS `export =` syntax to correctly support optional properties in the `ServerBuild` type, ensuring proper TypeScript compatibility with `exactOptionalPropertyTypes`.

integration/typegen-test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,31 @@ test.describe("typegen", () => {
490490
expect(proc.status).toBe(0);
491491
});
492492

493+
494+
test("works with tsconfig 'exactOptionalPropertyTypes' set to 'true'", async () => {
495+
const cwd = await createProject({
496+
"vite.config.ts": viteConfig,
497+
"app/routes.ts": tsx`
498+
import { type RouteConfig } from "@react-router/dev/routes";
499+
export default [] satisfies RouteConfig;
500+
`,
501+
"app/handler.ts": tsx`
502+
import { createRequestHandler } from "react-router";
503+
import * as serverBuild from "virtual:react-router/server-build";
504+
export default createRequestHandler(serverBuild);
505+
`,
506+
});
507+
508+
const tsconfig = await fse.readJson(path.join(cwd, "tsconfig.json"));
509+
tsconfig.compilerOptions.exactOptionalPropertyTypes = true;
510+
await fse.writeJson(path.join(cwd, "tsconfig.json"), tsconfig);
511+
512+
const proc = typecheck(cwd);
513+
expect(proc.stdout.toString()).toBe("");
514+
expect(proc.stderr.toString()).toBe("");
515+
expect(proc.status).toBe(0);
516+
});
517+
493518
test("dynamic import matches 'createRequestHandler' function argument type", async () => {
494519
const cwd = await createProject({
495520
"vite.config.ts": viteConfig,

packages/react-router-dev/typegen/index.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,18 @@ function register(ctx: Context) {
152152

153153
const virtual = ts`
154154
declare module "virtual:react-router/server-build" {
155-
import { ServerBuild } from "react-router";
156-
export const assets: ServerBuild["assets"];
157-
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
158-
export const basename: ServerBuild["basename"];
159-
export const entry: ServerBuild["entry"];
160-
export const future: ServerBuild["future"];
161-
export const isSpaMode: ServerBuild["isSpaMode"];
162-
export const prerender: ServerBuild["prerender"];
163-
export const publicPath: ServerBuild["publicPath"];
164-
export const routes: ServerBuild["routes"];
165-
export const ssr: ServerBuild["ssr"];
166-
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
155+
import type { ServerBuild } from "react-router";
156+
// Whilst this uses the CommonJS 'export =' syntax, which is technically not
157+
// ESM-compliant, it is intentionally implemented this way to properly support
158+
// optional properties in the ServerBuild type.
159+
//
160+
// TypeScript's type system does not provide an elegant solution for defining
161+
// optional exports at the module level in ESM syntax, so this approach offers
162+
// the most accurate type definitions for maintainability.
163+
//
164+
// This ensures all properties of ServerBuild, including optional ones, are
165+
// properly type-checked when using this module.
166+
const serverBuild: ServerBuild;
167+
export = serverBuild;
167168
}
168169
`;

0 commit comments

Comments
 (0)