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':