diff --git a/integration/helpers/vite-plugin-cloudflare-template/.gitignore b/integration/helpers/vite-plugin-cloudflare-template/.gitignore
index c08251ce0e..0c402dead2 100644
--- a/integration/helpers/vite-plugin-cloudflare-template/.gitignore
+++ b/integration/helpers/vite-plugin-cloudflare-template/.gitignore
@@ -1,6 +1,11 @@
-node_modules
+.DS_Store
+/node_modules/
+*.tsbuildinfo
-/.cache
-/build
-.env
-.react-router
+# React Router
+/.react-router/
+/build/
+
+# Cloudflare
+.mf
+.wrangler
diff --git a/integration/helpers/vite-plugin-cloudflare-template/env.d.ts b/integration/helpers/vite-plugin-cloudflare-template/env.d.ts
deleted file mode 100644
index 5e7dfe5dd9..0000000000
--- a/integration/helpers/vite-plugin-cloudflare-template/env.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
diff --git a/integration/helpers/vite-plugin-cloudflare-template/workers/app.ts b/integration/helpers/vite-plugin-cloudflare-template/workers/app.ts
index 879ce69b04..bf41f02ee2 100644
--- a/integration/helpers/vite-plugin-cloudflare-template/workers/app.ts
+++ b/integration/helpers/vite-plugin-cloudflare-template/workers/app.ts
@@ -14,7 +14,7 @@ declare module "react-router" {
}
const requestHandler = createRequestHandler(
- // @ts-expect-error - virtual module provided by React Router at build time
+ // @ts-expect-error
() => import("virtual:react-router/server-build"),
import.meta.env.MODE
);
diff --git a/playground/vite-plugin-cloudflare/.gitignore b/playground/vite-plugin-cloudflare/.gitignore
new file mode 100644
index 0000000000..0c402dead2
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/.gitignore
@@ -0,0 +1,11 @@
+.DS_Store
+/node_modules/
+*.tsbuildinfo
+
+# React Router
+/.react-router/
+/build/
+
+# Cloudflare
+.mf
+.wrangler
diff --git a/playground/vite-plugin-cloudflare/app/entry.server.tsx b/playground/vite-plugin-cloudflare/app/entry.server.tsx
new file mode 100644
index 0000000000..0d843dbb1f
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/app/entry.server.tsx
@@ -0,0 +1,43 @@
+import type { AppLoadContext, EntryContext } from "react-router";
+import { ServerRouter } from "react-router";
+import { isbot } from "isbot";
+import { renderToReadableStream } from "react-dom/server";
+
+export default async function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ _loadContext: AppLoadContext
+) {
+ let shellRendered = false;
+ const userAgent = request.headers.get("user-agent");
+
+ const body = await renderToReadableStream(
+ ,
+ {
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+ shellRendered = true;
+
+ // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
+ // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
+ if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
+ await body.allReady;
+ }
+
+ responseHeaders.set("Content-Type", "text/html");
+ return new Response(body, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ });
+}
diff --git a/playground/vite-plugin-cloudflare/app/root.tsx b/playground/vite-plugin-cloudflare/app/root.tsx
new file mode 100644
index 0000000000..b36392b4dd
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/app/root.tsx
@@ -0,0 +1,19 @@
+import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/playground/vite-plugin-cloudflare/app/routes.ts b/playground/vite-plugin-cloudflare/app/routes.ts
new file mode 100644
index 0000000000..4c05936cb6
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/app/routes.ts
@@ -0,0 +1,4 @@
+import { type RouteConfig } from "@react-router/dev/routes";
+import { flatRoutes } from "@react-router/fs-routes";
+
+export default flatRoutes() satisfies RouteConfig;
diff --git a/playground/vite-plugin-cloudflare/app/routes/_index.tsx b/playground/vite-plugin-cloudflare/app/routes/_index.tsx
new file mode 100644
index 0000000000..f6f63dcb51
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/app/routes/_index.tsx
@@ -0,0 +1,24 @@
+import type { MetaFunction } from "react-router";
+import type { Route } from "./+types/_index"
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: "New React Router App" },
+ { name: "description", content: "Welcome to React Router!" },
+ ];
+};
+
+export async function loader({ context }: Route.LoaderArgs) {
+ return {
+ message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE,
+ };
+}
+
+export default function Index({ loaderData }: Route.ComponentProps) {
+ return (
+
+
Welcome to React Router
+
{loaderData.message}
+
+ );
+}
diff --git a/playground/vite-plugin-cloudflare/package.json b/playground/vite-plugin-cloudflare/package.json
new file mode 100644
index 0000000000..2f3993a62e
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@playground/vite-plugin-cloudflare",
+ "version": "0.0.0",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "react-router dev",
+ "build": "react-router build",
+ "typecheck": "react-router typegen && tsc"
+ },
+ "dependencies": {
+ "express": "^4.19.2",
+ "isbot": "^5.1.11",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router": "workspace:*",
+ "serialize-javascript": "^6.0.1"
+ },
+ "devDependencies": {
+ "@cloudflare/vite-plugin": "^0.1.1",
+ "@cloudflare/workers-types": "^4.20250214.0",
+ "@react-router/dev": "workspace:*",
+ "@react-router/fs-routes": "workspace:*",
+ "@types/node": "^20.0.0",
+ "@types/react": "^18.2.20",
+ "@types/react-dom": "^18.2.7",
+ "eslint": "^8.38.0",
+ "typescript": "^5.1.6",
+ "vite": "^6.1.0",
+ "vite-tsconfig-paths": "^4.2.1",
+ "wrangler": "^3.109.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/playground/vite-plugin-cloudflare/public/favicon.ico b/playground/vite-plugin-cloudflare/public/favicon.ico
new file mode 100644
index 0000000000..5dbdfcddcb
Binary files /dev/null and b/playground/vite-plugin-cloudflare/public/favicon.ico differ
diff --git a/playground/vite-plugin-cloudflare/react-router.config.ts b/playground/vite-plugin-cloudflare/react-router.config.ts
new file mode 100644
index 0000000000..ef164520d8
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/react-router.config.ts
@@ -0,0 +1,7 @@
+import type { Config } from "@react-router/dev/config";
+
+export default {
+ future: {
+ unstable_viteEnvironmentApi: true,
+ },
+} satisfies Config;
diff --git a/playground/vite-plugin-cloudflare/tsconfig.cloudflare.json b/playground/vite-plugin-cloudflare/tsconfig.cloudflare.json
new file mode 100644
index 0000000000..587e895746
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/tsconfig.cloudflare.json
@@ -0,0 +1,28 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [
+ ".react-router/types/**/*",
+ "app/**/*",
+ "app/**/.server/**/*",
+ "app/**/.client/**/*",
+ "workers/**/*",
+ "worker-configuration.d.ts"
+ ],
+ "compilerOptions": {
+ "composite": true,
+ "strict": true,
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["@cloudflare/workers-types", "vite/client"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "baseUrl": ".",
+ "rootDirs": [".", "./.react-router/types"],
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "esModuleInterop": true,
+ "resolveJsonModule": true
+ }
+}
diff --git a/playground/vite-plugin-cloudflare/tsconfig.json b/playground/vite-plugin-cloudflare/tsconfig.json
new file mode 100644
index 0000000000..d7ce9e49b0
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.node.json" },
+ { "path": "./tsconfig.cloudflare.json" }
+ ],
+ "compilerOptions": {
+ "checkJs": true,
+ "verbatimModuleSyntax": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true
+ }
+}
diff --git a/playground/vite-plugin-cloudflare/tsconfig.node.json b/playground/vite-plugin-cloudflare/tsconfig.node.json
new file mode 100644
index 0000000000..4fce596ace
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/tsconfig.node.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["react-router.config.ts", "vite.config.ts"],
+ "compilerOptions": {
+ "composite": true,
+ "strict": true,
+ "types": ["node"],
+ "lib": ["ES2022"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/playground/vite-plugin-cloudflare/vite.config.ts b/playground/vite-plugin-cloudflare/vite.config.ts
new file mode 100644
index 0000000000..629cf373dc
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/vite.config.ts
@@ -0,0 +1,12 @@
+import { reactRouter } from "@react-router/dev/vite";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+import { cloudflare } from "@cloudflare/vite-plugin";
+
+export default defineConfig({
+ plugins: [
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
+ reactRouter(),
+ tsconfigPaths(),
+ ],
+});
diff --git a/playground/vite-plugin-cloudflare/worker-configuration.d.ts b/playground/vite-plugin-cloudflare/worker-configuration.d.ts
new file mode 100644
index 0000000000..421604f908
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/worker-configuration.d.ts
@@ -0,0 +1,5 @@
+// Generated by Wrangler by running `wrangler types`
+
+interface Env {
+ VALUE_FROM_CLOUDFLARE: "Hello from Cloudflare";
+}
diff --git a/playground/vite-plugin-cloudflare/workers/app.ts b/playground/vite-plugin-cloudflare/workers/app.ts
new file mode 100644
index 0000000000..bf41f02ee2
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/workers/app.ts
@@ -0,0 +1,28 @@
+import { createRequestHandler } from "react-router";
+
+declare global {
+ interface CloudflareEnvironment extends Env {}
+}
+
+declare module "react-router" {
+ export interface AppLoadContext {
+ cloudflare: {
+ env: CloudflareEnvironment;
+ ctx: ExecutionContext;
+ };
+ }
+}
+
+const requestHandler = createRequestHandler(
+ // @ts-expect-error
+ () => import("virtual:react-router/server-build"),
+ import.meta.env.MODE
+);
+
+export default {
+ async fetch(request, env, ctx) {
+ return requestHandler(request, {
+ cloudflare: { env, ctx },
+ });
+ },
+} satisfies ExportedHandler;
diff --git a/playground/vite-plugin-cloudflare/wrangler.toml b/playground/vite-plugin-cloudflare/wrangler.toml
new file mode 100644
index 0000000000..86700690f8
--- /dev/null
+++ b/playground/vite-plugin-cloudflare/wrangler.toml
@@ -0,0 +1,8 @@
+name = "react-router-app"
+compatibility_date = "2024-11-18"
+main = "./workers/app.ts"
+
+assets = {}
+
+[vars]
+VALUE_FROM_CLOUDFLARE = "Hello from Cloudflare"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 297b54e275..28325e133c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1341,6 +1341,64 @@ importers:
specifier: ^4.2.1
version: 4.3.2(typescript@5.4.5)(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0))
+ playground/vite-plugin-cloudflare:
+ dependencies:
+ express:
+ specifier: ^4.19.2
+ version: 4.19.2
+ isbot:
+ specifier: ^5.1.11
+ version: 5.1.11
+ react:
+ specifier: ^18.2.0
+ version: 18.2.0
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.2.0(react@18.2.0)
+ react-router:
+ specifier: workspace:*
+ version: link:../../packages/react-router
+ serialize-javascript:
+ specifier: ^6.0.1
+ version: 6.0.2
+ devDependencies:
+ '@cloudflare/vite-plugin':
+ specifier: ^0.1.1
+ version: 0.1.1(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0))(workerd@1.20241230.0)(wrangler@3.109.2(@cloudflare/workers-types@4.20250214.0))
+ '@cloudflare/workers-types':
+ specifier: ^4.20250214.0
+ version: 4.20250214.0
+ '@react-router/dev':
+ specifier: workspace:*
+ version: link:../../packages/react-router-dev
+ '@react-router/fs-routes':
+ specifier: workspace:*
+ version: link:../../packages/react-router-fs-routes
+ '@types/node':
+ specifier: ^20.0.0
+ version: 20.11.30
+ '@types/react':
+ specifier: ^18.2.18
+ version: 18.2.18
+ '@types/react-dom':
+ specifier: ^18.2.7
+ version: 18.2.7
+ eslint:
+ specifier: ^8.38.0
+ version: 8.57.0
+ typescript:
+ specifier: ^5.1.6
+ version: 5.4.5
+ vite:
+ specifier: ^6.1.0
+ version: 6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0)
+ vite-tsconfig-paths:
+ specifier: ^4.2.1
+ version: 4.3.2(typescript@5.4.5)(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0))
+ wrangler:
+ specifier: ^3.109.2
+ version: 3.109.2(@cloudflare/workers-types@4.20250214.0)
+
packages:
'@aashutoshrathi/word-wrap@1.2.6':