From f6293466c6242a91d75c0629e4f9dff2dd9cd18d Mon Sep 17 00:00:00 2001 From: Sora Morimoto Date: Sun, 30 Mar 2025 18:38:45 +0900 Subject: [PATCH 1/4] 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 --- .changeset/tiny-gifts-wash.md | 7 ++++++ integration/typegen-test.ts | 25 ++++++++++++++++++++++ packages/react-router-dev/typegen/index.ts | 25 +++++++++++----------- 3 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 .changeset/tiny-gifts-wash.md diff --git a/.changeset/tiny-gifts-wash.md b/.changeset/tiny-gifts-wash.md new file mode 100644 index 0000000000..dda2649cb3 --- /dev/null +++ b/.changeset/tiny-gifts-wash.md @@ -0,0 +1,7 @@ +--- +"@react-router/dev": patch +--- + +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`. diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts index 1460c528ab..8a255c3046 100644 --- a/integration/typegen-test.ts +++ b/integration/typegen-test.ts @@ -490,6 +490,31 @@ test.describe("typegen", () => { expect(proc.status).toBe(0); }); + + test("works with tsconfig 'exactOptionalPropertyTypes' set to 'true'", async () => { + const cwd = await createProject({ + "vite.config.ts": viteConfig, + "app/routes.ts": tsx` + import { type RouteConfig } from "@react-router/dev/routes"; + export default [] satisfies RouteConfig; + `, + "app/handler.ts": tsx` + import { createRequestHandler } from "react-router"; + import * as serverBuild from "virtual:react-router/server-build"; + export default createRequestHandler(serverBuild); + `, + }); + + const tsconfig = await fse.readJson(path.join(cwd, "tsconfig.json")); + tsconfig.compilerOptions.exactOptionalPropertyTypes = true; + await fse.writeJson(path.join(cwd, "tsconfig.json"), tsconfig); + + const proc = typecheck(cwd); + expect(proc.stdout.toString()).toBe(""); + expect(proc.stderr.toString()).toBe(""); + expect(proc.status).toBe(0); + }); + test("dynamic import matches 'createRequestHandler' function argument type", async () => { const cwd = await createProject({ "vite.config.ts": viteConfig, diff --git a/packages/react-router-dev/typegen/index.ts b/packages/react-router-dev/typegen/index.ts index 342f27f808..b78f635ddc 100644 --- a/packages/react-router-dev/typegen/index.ts +++ b/packages/react-router-dev/typegen/index.ts @@ -152,17 +152,18 @@ function register(ctx: Context) { const virtual = ts` declare module "virtual:react-router/server-build" { - import { ServerBuild } from "react-router"; - export const assets: ServerBuild["assets"]; - export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; - export const basename: ServerBuild["basename"]; - export const entry: ServerBuild["entry"]; - export const future: ServerBuild["future"]; - export const isSpaMode: ServerBuild["isSpaMode"]; - export const prerender: ServerBuild["prerender"]; - export const publicPath: ServerBuild["publicPath"]; - export const routes: ServerBuild["routes"]; - export const ssr: ServerBuild["ssr"]; - export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"]; + import type { ServerBuild } from "react-router"; + // Whilst this uses the CommonJS 'export =' syntax, which is technically not + // ESM-compliant, it is intentionally implemented this way to properly support + // optional properties in the ServerBuild type. + // + // TypeScript's type system does not provide an elegant solution for defining + // optional exports at the module level in ESM syntax, so this approach offers + // the most accurate type definitions for maintainability. + // + // This ensures all properties of ServerBuild, including optional ones, are + // properly type-checked when using this module. + const serverBuild: ServerBuild; + export = serverBuild; } `; From 017b8cb4d234d4e7eec07fa26103009ef826f5d2 Mon Sep 17 00:00:00 2001 From: Sora Morimoto Date: Sun, 30 Mar 2025 18:39:15 +0900 Subject: [PATCH 2/4] Add @smorimoto to contributors list Signed-off-by: Sora Morimoto --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index dd0d8c165f..4cb0e8c96a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -303,6 +303,7 @@ - SkayuX - skratchdot - smithki +- smorimoto - soartec-lab - sorokya - sorrycc From 56149e6e865460c5a3c7eb966dfc64629be2c83e Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 4 Apr 2025 15:51:41 +1100 Subject: [PATCH 3/4] Update changeset --- .changeset/tiny-gifts-wash.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.changeset/tiny-gifts-wash.md b/.changeset/tiny-gifts-wash.md index dda2649cb3..5f469ea3d1 100644 --- a/.changeset/tiny-gifts-wash.md +++ b/.changeset/tiny-gifts-wash.md @@ -2,6 +2,4 @@ "@react-router/dev": patch --- -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`. +Update generated types for `virtual:react-router/server-build` to ensure compatibility with TypeScript's `exactOptionalPropertyTypes` option From 4d600f50a95c3c942be41c1baebc26322f653cbd Mon Sep 17 00:00:00 2001 From: Mark Dalgleish Date: Fri, 4 Apr 2025 16:39:09 +1100 Subject: [PATCH 4/4] Implement alternate fix via `ServerBuild` type --- .changeset/tiny-gifts-wash.md | 4 +-- integration/typegen-test.ts | 1 - packages/react-router-dev/typegen/index.ts | 25 +++++++++---------- .../react-router/lib/server-runtime/build.ts | 14 ++++++++--- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.changeset/tiny-gifts-wash.md b/.changeset/tiny-gifts-wash.md index 5f469ea3d1..7b0a67767e 100644 --- a/.changeset/tiny-gifts-wash.md +++ b/.changeset/tiny-gifts-wash.md @@ -1,5 +1,5 @@ --- -"@react-router/dev": patch +"react-router": patch --- -Update generated types for `virtual:react-router/server-build` to ensure compatibility with TypeScript's `exactOptionalPropertyTypes` option +Update `ServerBuild` type to ensure compatibility with TypeScript's `exactOptionalPropertyTypes` option diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts index 8a255c3046..1115315c93 100644 --- a/integration/typegen-test.ts +++ b/integration/typegen-test.ts @@ -490,7 +490,6 @@ test.describe("typegen", () => { expect(proc.status).toBe(0); }); - test("works with tsconfig 'exactOptionalPropertyTypes' set to 'true'", async () => { const cwd = await createProject({ "vite.config.ts": viteConfig, diff --git a/packages/react-router-dev/typegen/index.ts b/packages/react-router-dev/typegen/index.ts index b78f635ddc..5db028c324 100644 --- a/packages/react-router-dev/typegen/index.ts +++ b/packages/react-router-dev/typegen/index.ts @@ -152,18 +152,17 @@ function register(ctx: Context) { const virtual = ts` declare module "virtual:react-router/server-build" { - import type { ServerBuild } from "react-router"; - // Whilst this uses the CommonJS 'export =' syntax, which is technically not - // ESM-compliant, it is intentionally implemented this way to properly support - // optional properties in the ServerBuild type. - // - // TypeScript's type system does not provide an elegant solution for defining - // optional exports at the module level in ESM syntax, so this approach offers - // the most accurate type definitions for maintainability. - // - // This ensures all properties of ServerBuild, including optional ones, are - // properly type-checked when using this module. - const serverBuild: ServerBuild; - export = serverBuild; + import type { ServerBuild } from "react-router"; import { ServerBuild } from "react-router"; + export const assets: ServerBuild["assets"]; + export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"]; + export const basename: ServerBuild["basename"]; + export const entry: ServerBuild["entry"]; + export const future: ServerBuild["future"]; + export const isSpaMode: ServerBuild["isSpaMode"]; + export const prerender: ServerBuild["prerender"]; + export const publicPath: ServerBuild["publicPath"]; + export const routes: ServerBuild["routes"]; + export const ssr: ServerBuild["ssr"]; + export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"]; } `; diff --git a/packages/react-router/lib/server-runtime/build.ts b/packages/react-router/lib/server-runtime/build.ts index 1fd78abdb7..9502fd4150 100644 --- a/packages/react-router/lib/server-runtime/build.ts +++ b/packages/react-router/lib/server-runtime/build.ts @@ -24,14 +24,20 @@ export interface ServerBuild { }; routes: ServerRouteManifest; assets: AssetsManifest; - basename?: string; + // `| undefined` is required to ensure compatibility with TypeScript's + // `exactOptionalPropertyTypes` option + basename?: string | undefined; publicPath: string; assetsBuildDirectory: string; future: FutureConfig; ssr: boolean; - unstable_getCriticalCss?: (args: { - pathname: string; - }) => OptionalCriticalCss | Promise; + unstable_getCriticalCss?: + | ((args: { + pathname: string; + }) => OptionalCriticalCss | Promise) + // `| undefined` is required to ensure compatibility with TypeScript's + // `exactOptionalPropertyTypes` option + | undefined; /** * @deprecated This is now done via a custom header during prerendering */