├── .gitignore ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── prerender.ts ├── public └── favicon.ico ├── src ├── app │ ├── app.client.tsx │ ├── app.css │ ├── app.tsx │ ├── components │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── global-loader.tsx │ │ │ ├── input.tsx │ │ │ ├── select.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── textarea.tsx │ │ │ └── validated-form.tsx │ ├── global-actions.ts │ ├── lib │ │ └── utils.ts │ ├── login │ │ ├── login.client.tsx │ │ ├── login.shared.ts │ │ └── login.tsx │ ├── signup │ │ ├── signup.client.tsx │ │ ├── signup.shared.ts │ │ └── signup.tsx │ └── todo │ │ ├── todo.client.tsx │ │ ├── todo.shared.ts │ │ └── todo.tsx ├── browser │ └── entry.browser.tsx ├── framework │ ├── browser.tsx │ ├── client.ts │ ├── cookie-session.ts │ ├── cookies.ts │ ├── crypto.ts │ ├── references.browser.ts │ ├── references.server.ts │ ├── references.ssr.ts │ ├── server.ts │ ├── sessions.ts │ ├── ssr.tsx │ └── warnings.ts ├── server │ ├── .dev.vars │ ├── entry.server.tsx │ ├── todo-list.ts │ ├── user.ts │ └── wrangler.toml └── ssr │ ├── entry.ssr.tsx │ └── wrangler.toml ├── tailwind.config.ts ├── tsconfig.client.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .wrangler 3 | build 4 | dist 5 | logs 6 | node_modules 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@playground/react-server", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "vite build --app", 7 | "check:types": "tsc --build", 8 | "deploy": "wrangler deploy -c dist/server/wrangler.json && pnpm wrangler deploy -c dist/ssr/wrangler.json ", 9 | "dev": "vite dev", 10 | "prerender": "node --experimental-strip-types ./prerender.ts", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@jacob-ebey/react-server-dom-vite": "19.0.0-experimental.14", 15 | "@radix-ui/react-slot": "^1.1.1", 16 | "bcrypt-edge": "^0.1.0", 17 | "class-variance-authority": "^0.7.1", 18 | "clsx": "^2.1.1", 19 | "cookie": "^1.0.2", 20 | "focus-trap": "^7.6.2", 21 | "lucide-react": "^0.469.0", 22 | "pouchdb-browser": "^9.0.0", 23 | "react": "^19.0.0", 24 | "react-aria-components": "^1.5.0", 25 | "react-dom": "^19.0.0", 26 | "tailwind-merge": "^2.6.0", 27 | "valibot": "1.0.0-beta.9" 28 | }, 29 | "devDependencies": { 30 | "@cloudflare/workers-types": "^4.20241230.0", 31 | "@flarelabs-net/vite-plugin-cloudflare": "https://pkg.pr.new/flarelabs-net/vite-plugin-cloudflare/@flarelabs-net/vite-plugin-cloudflare@123", 32 | "@jacob-ebey/vite-react-server-dom": "0.0.12", 33 | "@types/cookie": "^1.0.0", 34 | "@types/dom-navigation": "^1.0.4", 35 | "@types/node": "^22.10.4", 36 | "@types/react": "^19.0.0", 37 | "@types/react-dom": "^19.0.0", 38 | "autoprefixer": "^10.4.20", 39 | "execa": "^9.5.2", 40 | "postcss": "^8.4.49", 41 | "rsc-html-stream": "0.0.4", 42 | "tailwindcss": "^3.4.17", 43 | "typescript": "^5.7.2", 44 | "unenv": "npm:unenv-nightly@2.0.0-20241204-140205-a5d5190", 45 | "unplugin-rsc": "0.0.11", 46 | "vite": "^6.0.7", 47 | "vite-plugin-pwa": "^0.21.1", 48 | "vite-tsconfig-paths": "^5.1.4", 49 | "wrangler": "^3.99.0" 50 | }, 51 | "pnpm": { 52 | "overrides": { 53 | "wrangler": "^3.99.0", 54 | "vite": "^6.0.7" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('tailwindcss'), require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /prerender.ts: -------------------------------------------------------------------------------- 1 | // Dependant on https://github.com/flarelabs-net/vite-plugin-cloudflare/pull/125 2 | 3 | import * as fsp from "node:fs/promises"; 4 | import * as path from "node:path"; 5 | 6 | import { $ } from "execa"; 7 | 8 | const PATHS_TO_PRERENDER = ["/", "/signup"]; 9 | 10 | const port = 4173; 11 | const host = "localhost"; 12 | const proc = $`pnpm preview --host ${host} --port ${port}`; 13 | 14 | async function waitForPort() { 15 | const timeout = 10000; 16 | 17 | const start = Date.now(); 18 | while (Date.now() - start < timeout) { 19 | try { 20 | await fetch(`http://${host}:${port}`); 21 | return; 22 | } catch (error) { 23 | await new Promise((resolve) => setTimeout(resolve, 100)); 24 | } 25 | } 26 | } 27 | 28 | try { 29 | await waitForPort(); 30 | 31 | for (const pathname of PATHS_TO_PRERENDER) { 32 | console.log(`prerendering ${pathname}`); 33 | const response = await fetch( 34 | new URL(pathname, `http://${host}:${port}`).href, 35 | { 36 | headers: { 37 | PRERENDER: "1", 38 | }, 39 | } 40 | ); 41 | 42 | const rscPayload = decodeURI(response.headers.get("X-RSC-Payload") || ""); 43 | 44 | if (response.status !== 200 || !rscPayload) { 45 | throw new Error(`Failed to prerender rsc payload for ${pathname}`); 46 | } 47 | 48 | const html = await response.text(); 49 | if (!html.includes("")) { 50 | throw new Error(`Failed to prerender html for ${pathname}`); 51 | } 52 | 53 | const segments = pathname.split("/").filter(Boolean); 54 | if (segments.length === 0) { 55 | segments.push("index"); 56 | } 57 | const lastSegment = segments.pop(); 58 | const rscPath = path.join( 59 | "dist", 60 | "client", 61 | "_prerender", 62 | ...segments, 63 | lastSegment + ".data" 64 | ); 65 | const htmlPath = path.join( 66 | "dist", 67 | "client", 68 | "_prerender", 69 | ...segments, 70 | lastSegment + ".html" 71 | ); 72 | 73 | await fsp.mkdir(path.dirname(rscPath), { recursive: true }); 74 | 75 | await Promise.all([ 76 | fsp.writeFile(rscPath, rscPayload), 77 | fsp.writeFile(htmlPath, html), 78 | ]); 79 | } 80 | } finally { 81 | proc.kill(); 82 | } 83 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/cf-react-server-template/19ed0b9bc6638eec4789c3a75827ed9e06487cfa/public/favicon.ico -------------------------------------------------------------------------------- /src/app/app.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useNavigating } from "framework/client"; 4 | 5 | import { GlobalLoader } from "~/components/ui/global-loader"; 6 | 7 | export function GlobalPendingIndicator() { 8 | const navigating = useNavigating(); 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --sidebar-width-mobile: 80%; 8 | --sidebar-width-desktop: 20rem; 9 | 10 | --background: white; 11 | --foreground: black; 12 | --foreground-muted: #666; 13 | --border: black; 14 | --primary: blue; 15 | --primary-foreground: white; 16 | --destructive: red; 17 | --destructive-foreground: white; 18 | 19 | @media (prefers-color-scheme: dark) { 20 | --background: black; 21 | --foreground: white; 22 | --foreground-muted: #999; 23 | --border: white; 24 | --primary: blue; 25 | --primary-foreground: white; 26 | --destructive: red; 27 | --destructive-foreground: white; 28 | } 29 | } 30 | 31 | *:focus-visible { 32 | @apply outline-primary; 33 | } 34 | 35 | html, 36 | body { 37 | @apply bg-background text-foreground subpixel-antialiased; 38 | 39 | @media (prefers-color-scheme: dark) { 40 | color-scheme: dark; 41 | } 42 | } 43 | } 44 | 45 | @layer components { 46 | .typography { 47 | @apply text-sm lg:text-base; 48 | } 49 | 50 | .typography hr, 51 | .hr { 52 | @apply border border-border my-6; 53 | } 54 | 55 | .typography h1, 56 | .h1 { 57 | @apply text-2xl md:text-3xl font-bold [&:not(:first-child)]:mt-10 mb-6; 58 | } 59 | 60 | .typography h2, 61 | .h2 { 62 | @apply text-xl md:text-2xl font-bold [&:not(:first-child)]:mt-10 mb-6; 63 | } 64 | 65 | .typography h3, 66 | .h3 { 67 | @apply text-lg md:text-xl font-bold [&:not(:first-child)]:mt-10 mb-6; 68 | } 69 | 70 | .typography h4, 71 | .h4 { 72 | @apply text-base md:text-lg font-bold [&:not(:first-child)]:mt-10 mb-6; 73 | } 74 | 75 | .typography h5, 76 | .h5 { 77 | @apply text-sm lg:text-base font-bold [&:not(:first-child)]:mt-10 mb-6; 78 | } 79 | 80 | .typography h6, 81 | .h6 { 82 | @apply text-xs lg:text-sm font-bold [&:not(:first-child)]:mt-10 mb-6; 83 | } 84 | 85 | .typography p, 86 | .p { 87 | @apply mb-6 [&:not(:first-child)]:mt-6; 88 | } 89 | 90 | .typography a, 91 | .a { 92 | @apply underline decoration-2; 93 | } 94 | 95 | .typography abbr, 96 | .typography del, 97 | .typography ins { 98 | @apply decoration-2; 99 | } 100 | 101 | .typography ul { 102 | @apply list-disc pl-4 mb-6 [&:not(:first-child)]:mt-6 space-y-2; 103 | } 104 | 105 | .typography ol { 106 | @apply list-decimal pl-7 mb-6 [&:not(:first-child)]:mt-6 space-y-2; 107 | } 108 | 109 | .typography dl { 110 | @apply space-y-2 space-y-2; 111 | } 112 | 113 | .typography dt { 114 | @apply font-bold; 115 | } 116 | 117 | .typography dd { 118 | @apply ml-4; 119 | } 120 | 121 | .typography ul ul, 122 | .typography ol ul, 123 | .typography ol ol, 124 | .typography ul ol { 125 | @apply my-0; 126 | } 127 | 128 | .typography blockquote { 129 | @apply border-l-4 border-border pl-4 italic mb-6 [&:not(:first-child)]:mt-6; 130 | } 131 | 132 | .typography pre { 133 | @apply mb-6 [&:not(:first-child)]:mt-6; 134 | } 135 | 136 | .typography pre:has(code) { 137 | @apply overflow-x-auto p-4 border-2 border-border; 138 | } 139 | 140 | .typography code { 141 | @apply font-mono text-base; 142 | } 143 | 144 | .typography table, 145 | .table { 146 | @apply w-full border-x-2 border-t-2 border-border mb-6 [&:not(:first-child)]:mt-6; 147 | } 148 | 149 | .typography thead, 150 | .table thead { 151 | @apply font-bold text-left; 152 | } 153 | 154 | .typography tr, 155 | .table tr { 156 | @apply border-b-2 border-border; 157 | } 158 | 159 | .typography th, 160 | .table th { 161 | @apply font-bold border-r-2 border-border p-2 align-top; 162 | } 163 | 164 | .typography td, 165 | .table td { 166 | @apply border-r-2 border-border p-2 align-top; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | destoryCookieSession, 3 | getCookieSession, 4 | getURL, 5 | } from "framework/server"; 6 | 7 | import { GlobalPendingIndicator } from "./app.client"; 8 | import stylesHref from "./app.css?url"; 9 | 10 | import Login from "./login/login"; 11 | import Signup from "./signup/signup"; 12 | import Todo from "./todo/todo"; 13 | 14 | export function App() { 15 | const url = getURL(); 16 | 17 | return ( 18 | 19 | 20 | 21 | React Server 22 | 23 | 24 | 25 | 26 | {(() => { 27 | const pathStart = url.pathname.split("/", 2).join("/"); 28 | switch (pathStart) { 29 | case "/todo": 30 | return ; 31 | case "/signup": 32 | return ; 33 | default: 34 | return ; 35 | } 36 | })()} 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import { cn } from '~/lib/utils'; 5 | import { cva } from 'class-variance-authority'; 6 | import { Button as BaseButton } from 'react-aria-components'; 7 | import type { VariantProps } from 'class-variance-authority'; 8 | import type { ButtonProps as BaseButtonProps } from 'react-aria-components'; 9 | 10 | export const buttonVariants = cva( 11 | cn( 12 | 'inline-flex items-center justify-center gap-2 whitepsace-nowrap [&_svg]:pointer-events-none [&_svg]:size-5 [&_svg]:shrink-0', 13 | 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50', 14 | ), 15 | { 16 | variants: { 17 | size: { 18 | sm: 'px-2 py-1 text-base', 19 | md: 'px-2 py-1 text-lg', 20 | lg: 'px-3 py-2 text-xl', 21 | icon: 'h-9 w-9 aspect-square', 22 | }, 23 | variant: { 24 | default: 25 | 'font-bold bg-background text-foreground border-2 border-border', 26 | primary: 27 | 'font-bold bg-primary text-primary-foreground border-2 border-border', 28 | destructive: 29 | 'font-bold bg-background text-destructive border-2 border-destructive focus-visible:ring-destructive', 30 | }, 31 | }, 32 | defaultVariants: { 33 | size: 'md', 34 | variant: 'default', 35 | }, 36 | }, 37 | ); 38 | 39 | export type ButtonProps = React.ComponentProps<'button'> & 40 | BaseButtonProps & 41 | VariantProps & { 42 | asChild?: boolean; 43 | }; 44 | 45 | export function Button({ 46 | asChild, 47 | className, 48 | disabled, 49 | onKeyDown, 50 | size, 51 | variant, 52 | ...props 53 | }: ButtonProps) { 54 | const Comp: any = asChild ? Slot : BaseButton; 55 | return ( 56 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/app/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | export type CheckboxProps = React.ComponentProps<"input">; 4 | 5 | export function Checkbox({ className, ...props }: CheckboxProps) { 6 | return ( 7 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/ui/global-loader.tsx: -------------------------------------------------------------------------------- 1 | export type GlobalLoaderProps = { 2 | loading: boolean; 3 | }; 4 | 5 | export function GlobalLoader({ loading }: GlobalLoaderProps) { 6 | if (!loading) { 7 | return null; 8 | } 9 | 10 | return ( 11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '~/lib/utils'; 2 | 3 | export type InputProps = React.ComponentProps<'input'>; 4 | 5 | export function Input({ className, ...props }: InputProps) { 6 | return ( 7 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | export type SelectProps = React.ComponentProps<"select">; 4 | 5 | export function Select({ className, ...props }: SelectProps) { 6 | return ( 7 |