diff --git a/examples/react-router-framework-and-antd/.dockerignore b/examples/react-router-framework-and-antd/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/examples/react-router-framework-and-antd/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/examples/react-router-framework-and-antd/.gitignore b/examples/react-router-framework-and-antd/.gitignore new file mode 100644 index 0000000..510be82 --- /dev/null +++ b/examples/react-router-framework-and-antd/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/node_modules/ + +# React Router +/.react-router/ +/build/ \ No newline at end of file diff --git a/examples/react-router-framework-and-antd/Dockerfile b/examples/react-router-framework-and-antd/Dockerfile new file mode 100644 index 0000000..207bf93 --- /dev/null +++ b/examples/react-router-framework-and-antd/Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:20-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:20-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:20-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/examples/react-router-framework-and-antd/README.md b/examples/react-router-framework-and-antd/README.md new file mode 100644 index 0000000..a676264 --- /dev/null +++ b/examples/react-router-framework-and-antd/README.md @@ -0,0 +1,14 @@ +# React Router + TypeScript + Vite + +## Development + +```bash +npm install && npm run dev +``` + +## Docs + +- [Vite](https://vitejs.dev/) +- [React](https://react.dev/) +- [Ant Design](https://ant.design/) +- [React Router docs](https://reactrouter.com/) \ No newline at end of file diff --git a/examples/react-router-framework-and-antd/app/app.css b/examples/react-router-framework-and-antd/app/app.css new file mode 100644 index 0000000..99345d8 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/app.css @@ -0,0 +1,15 @@ +@import "tailwindcss"; + +@theme { + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/examples/react-router-framework-and-antd/app/entry.client.tsx b/examples/react-router-framework-and-antd/app/entry.client.tsx new file mode 100644 index 0000000..f8d06ad --- /dev/null +++ b/examples/react-router-framework-and-antd/app/entry.client.tsx @@ -0,0 +1,18 @@ +import { createCache, StyleProvider } from "@ant-design/cssinjs"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; +import { HydratedRouter } from "react-router/dom"; + +// used to ensure proper hydration and consistent styling between server and client for Ant Design components +const cache = createCache(); + +startTransition(() => { + hydrateRoot( + document, + + + + + + ); +}); diff --git a/examples/react-router-framework-and-antd/app/entry.server.tsx b/examples/react-router-framework-and-antd/app/entry.server.tsx new file mode 100644 index 0000000..7f31215 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/entry.server.tsx @@ -0,0 +1,97 @@ +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "react-router"; +import { createReadableStreamFromReadable } from "@react-router/node"; +import { ServerRouter } from "react-router"; +import { isbot } from "isbot"; +import type { RenderToPipeableStreamOptions } from "react-dom/server"; +import { renderToPipeableStream } from "react-dom/server"; +import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs"; + +export const streamTimeout = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext + // If you have middleware enabled: + // loadContext: unstable_RouterContextProvider +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + let userAgent = request.headers.get("user-agent"); + const cache = createCache(); + let styles = ""; + + // 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 + let readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode + ? "onAllReady" + : "onShellReady"; + + const { pipe, abort } = renderToPipeableStream( + + + , + { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + // Extract styles from the cache + styles = extractStyle(cache); + + // We need to transform the output to inject the Ant Design styles + let didInjectStyles = false; + const wrappedStream = new TransformStream({ + transform(chunk, controller) { + const chunkString = new TextDecoder().decode(chunk); + if (!didInjectStyles && chunkString.includes("")) { + // Inject the styles before the closing head tag + const modifiedChunk = chunkString.replace( + "", + `` + ); + didInjectStyles = true; + controller.enqueue(new TextEncoder().encode(modifiedChunk)); + } else { + controller.enqueue(chunk); + } + }, + }); + + resolve( + new Response(stream.pipeThrough(wrappedStream), { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + 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); + } + }, + } + ); + + // Abort the rendering stream after the `streamTimeout` so it has time to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); + }); +} diff --git a/examples/react-router-framework-and-antd/app/root.tsx b/examples/react-router-framework-and-antd/app/root.tsx new file mode 100644 index 0000000..fab9ab9 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/root.tsx @@ -0,0 +1,92 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; +import { ConfigProvider } from "antd"; + +import type { Route } from "./+types/root"; +import "./app.css"; + +export const links: Route.LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, + { + rel: "icon", + href: "/public/favicon.ico", + sizes: "48x48", + type: "image/vnd.microsoft.icon", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + {children} + + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/examples/react-router-framework-and-antd/app/routes.ts b/examples/react-router-framework-and-antd/app/routes.ts new file mode 100644 index 0000000..102b402 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from "@react-router/dev/routes"; + +export default [index("routes/home.tsx")] satisfies RouteConfig; diff --git a/examples/react-router-framework-and-antd/app/routes/home.tsx b/examples/react-router-framework-and-antd/app/routes/home.tsx new file mode 100644 index 0000000..398e47c --- /dev/null +++ b/examples/react-router-framework-and-antd/app/routes/home.tsx @@ -0,0 +1,13 @@ +import type { Route } from "./+types/home"; +import { Welcome } from "../welcome/welcome"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "New React Router App" }, + { name: "description", content: "Welcome to React Router!" }, + ]; +} + +export default function Home() { + return ; +} diff --git a/examples/react-router-framework-and-antd/app/welcome/logo-dark.svg b/examples/react-router-framework-and-antd/app/welcome/logo-dark.svg new file mode 100644 index 0000000..dd82028 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/welcome/logo-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/react-router-framework-and-antd/app/welcome/logo-light.svg b/examples/react-router-framework-and-antd/app/welcome/logo-light.svg new file mode 100644 index 0000000..7328492 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/welcome/logo-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/react-router-framework-and-antd/app/welcome/welcome.tsx b/examples/react-router-framework-and-antd/app/welcome/welcome.tsx new file mode 100644 index 0000000..8f386e4 --- /dev/null +++ b/examples/react-router-framework-and-antd/app/welcome/welcome.tsx @@ -0,0 +1,95 @@ +import { Button, Col, Row } from "antd"; +import logoDark from "./logo-dark.svg"; +import logoLight from "./logo-light.svg"; + +export function Welcome() { + return ( +
+
+
+
+ React Router + React Router +
+
+
+ +
+
+
+ ); +} + +const resources = [ + { + href: "https://reactrouter.com/docs", + text: "React Router Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/examples/react-router-framework-and-antd/package.json b/examples/react-router-framework-and-antd/package.json new file mode 100644 index 0000000..45462a2 --- /dev/null +++ b/examples/react-router-framework-and-antd/package.json @@ -0,0 +1,33 @@ +{ + "name": "react-router-framework-and-antd", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, + "dependencies": { + "@ant-design/cssinjs": "^1.23.0", + "@react-router/node": "^7.3.0", + "@react-router/serve": "^7.3.0", + "antd": "^5.24.3", + "isbot": "^5.1.17", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.3.0" + }, + "devDependencies": { + "@react-router/dev": "^7.3.0", + "@tailwindcss/vite": "^4.0.0", + "@types/node": "^20", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.1", + "react-router-devtools": "^1.1.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.7.2", + "vite": "^5.4.11", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/examples/react-router-framework-and-antd/public/favicon.ico b/examples/react-router-framework-and-antd/public/favicon.ico new file mode 100644 index 0000000..5dbdfcd Binary files /dev/null and b/examples/react-router-framework-and-antd/public/favicon.ico differ diff --git a/examples/react-router-framework-and-antd/react-router.config.ts b/examples/react-router-framework-and-antd/react-router.config.ts new file mode 100644 index 0000000..6ff16f9 --- /dev/null +++ b/examples/react-router-framework-and-antd/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/examples/react-router-framework-and-antd/tsconfig.json b/examples/react-router-framework-and-antd/tsconfig.json new file mode 100644 index 0000000..dc391a4 --- /dev/null +++ b/examples/react-router-framework-and-antd/tsconfig.json @@ -0,0 +1,27 @@ +{ + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/examples/react-router-framework-and-antd/vite.config.ts b/examples/react-router-framework-and-antd/vite.config.ts new file mode 100644 index 0000000..eb8a1e3 --- /dev/null +++ b/examples/react-router-framework-and-antd/vite.config.ts @@ -0,0 +1,11 @@ +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], + server: { + open: true, + }, +});