├── bunfig.toml ├── src ├── react.svg ├── index.html ├── App.tsx ├── frontend.tsx ├── index.tsx ├── APITester.tsx ├── logo.svg └── index.css ├── README.md ├── tsconfig.json ├── .gitignore ├── functions └── [[path]].ts ├── package.json ├── shim ├── bridge.ts └── router.ts └── bun.lock /bunfig.toml: -------------------------------------------------------------------------------- 1 | [serve.static] 2 | env = "BUN_PUBLIC_*" -------------------------------------------------------------------------------- /src/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bun-react-template 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To start a development server: 10 | 11 | ```bash 12 | bun dev 13 | ``` 14 | 15 | To run for production: 16 | 17 | ```bash 18 | bun start 19 | ``` 20 | 21 | This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "jsx": "react-jsx", 7 | "allowJs": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["./src/*"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bun + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | 36 | # wrangler files 37 | .wrangler 38 | .dev.vars 39 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { APITester } from "./APITester"; 3 | 4 | import logo from "./logo.svg"; 5 | import reactLogo from "./react.svg"; 6 | 7 | export function App() { 8 | return ( 9 |
10 |
11 | Bun Logo 12 | React Logo 13 |
14 | 15 |

Bun + React + Cloudflare Pages

16 |

17 | Edit src/App.tsx and save to test HMR 18 |

19 | 20 |
21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/frontend.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is the entry point for the React app, it sets up the root 3 | * element and renders the App component to the DOM. 4 | * 5 | * It is included in `src/index.html`. 6 | */ 7 | 8 | import { createRoot } from "react-dom/client"; 9 | import { StrictMode } from "react"; 10 | import { App } from "./App"; 11 | 12 | const elem = document.getElementById("root")!; 13 | const app = ( 14 | 15 | 16 | 17 | ); 18 | 19 | if (import.meta.hot) { 20 | // With hot module reloading, `import.meta.hot.data` is persisted. 21 | const root = (import.meta.hot.data.root ??= createRoot(elem)); 22 | root.render(app); 23 | } else { 24 | // The hot module reloading API is not available in production. 25 | createRoot(elem).render(app); 26 | } 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { serve } from "bun"; 2 | import index from "./index.html"; 3 | 4 | const server = serve({ 5 | routes: { 6 | // Serve index.html for all unmatched routes. 7 | "/*": index, 8 | 9 | "/api/hello": { 10 | async GET(req) { 11 | return Response.json({ 12 | message: "Hello, world!", 13 | method: "GET", 14 | }); 15 | }, 16 | async PUT(req) { 17 | return Response.json({ 18 | message: "Hello, world!", 19 | method: "PUT", 20 | }); 21 | }, 22 | }, 23 | 24 | "/api/hello/:name": async (req) => { 25 | const name = req.params.name; 26 | return Response.json({ 27 | message: `Hello, ${name}!`, 28 | }); 29 | }, 30 | }, 31 | 32 | development: process.env.NODE_ENV !== "production", 33 | }); 34 | 35 | console.log(`🚀 Server running at ${server.url}`); 36 | -------------------------------------------------------------------------------- /functions/[[path]].ts: -------------------------------------------------------------------------------- 1 | import type { PagesFunction } from '@cloudflare/workers-types' 2 | import type { BunRequest, RouterTypes } from 'bun' 3 | import { getRouter, stubServer } from '../shim/bridge' 4 | 5 | export const onRequest: PagesFunction = async (context) => { 6 | const router = await getRouter() 7 | const { method, url } = context.request 8 | const { pathname } = new URL(url) 9 | const { handler, params } = router.find(pathname) 10 | 11 | if (handler instanceof Response) { 12 | return handler.clone() 13 | } 14 | 15 | if (handler) { 16 | const methods = typeof handler === 'function' ? { GET: handler } : handler 17 | const fn = methods[method as RouterTypes.HTTPMethod] 18 | if (fn) { 19 | const req: BunRequest = context.request as any 20 | req.params = params 21 | return fn(req, stubServer) as any 22 | } 23 | } 24 | 25 | return context.next() 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-react-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "src/index.tsx", 7 | "module": "src/index.tsx", 8 | "scripts": { 9 | "dev": "bun --hot src/index.tsx", 10 | "build": "bun build:client && bun build:server", 11 | "build:client": "rm -rf dist/client && bun build src/index.html --outdir dist/client --target browser --define process.env.NODE_ENV='\"production\"' --env 'BUN_PUBLIC_*'", 12 | "build:server": "rm -rf dist/server && bun build src/index.tsx --outdir dist/server --target bun --define process.env.NODE_ENV='\"production\"' --env 'BUN_PUBLIC_*'", 13 | "start": "NODE_ENV=production bun src/index.tsx", 14 | "pages": "bun run build && wrangler pages dev ./dist/client", 15 | "golive": "bun run build && wrangler pages deploy ./dist/client" 16 | }, 17 | "dependencies": { 18 | "react": "^19", 19 | "react-dom": "^19" 20 | }, 21 | "devDependencies": { 22 | "@cloudflare/workers-types": "^4.20250214.0", 23 | "@types/bun": "latest", 24 | "@types/react": "^19", 25 | "@types/react-dom": "^19", 26 | "wrangler": "^3.109.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shim/bridge.ts: -------------------------------------------------------------------------------- 1 | import type { Server, serve } from 'bun' 2 | import { Router } from './router' 3 | 4 | let loader: Promise 5 | export function getRouter() { 6 | if (!loader) { 7 | const router = new Router() 8 | globalThis.Bun = { 9 | serve(options: Parameters[0]) { 10 | if ('routes' in options && options.routes) { 11 | for (const [path, conf] of Object.entries(options.routes)) { 12 | router.add(path, conf) 13 | } 14 | } 15 | return stubServer 16 | } 17 | } as any 18 | loader = import('../dist/server/index.js').then(() => router) 19 | } 20 | return loader 21 | } 22 | 23 | getRouter().catch(err => { 24 | console.error('Failed to load router:', err) 25 | }) 26 | 27 | export const stubServer: Server = { 28 | url: new URL('http://stub.server'), 29 | stop: noimpl, 30 | reload: noimpl, 31 | fetch: noimpl, 32 | upgrade: noimpl, 33 | publish: noimpl, 34 | subscriberCount: noimpl, 35 | requestIP: noimpl, 36 | timeout: noimpl, 37 | ref: noimpl, 38 | unref: noimpl, 39 | pendingRequests: NaN, 40 | pendingWebSockets: NaN, 41 | port: NaN, 42 | hostname: 'stub.server', 43 | development: false, 44 | id: 'stub.server', 45 | [Symbol.dispose]: noimpl 46 | } 47 | 48 | function noimpl(): never { 49 | throw new Error('[stubserver] Not implemented') 50 | } 51 | -------------------------------------------------------------------------------- /src/APITester.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, type FormEvent } from "react"; 2 | 3 | export function APITester() { 4 | const responseInputRef = useRef(null); 5 | 6 | const testEndpoint = async (e: FormEvent) => { 7 | e.preventDefault(); 8 | 9 | try { 10 | const form = e.currentTarget; 11 | const formData = new FormData(form); 12 | const endpoint = formData.get("endpoint") as string; 13 | const url = new URL(endpoint, location.href); 14 | const method = formData.get("method") as string; 15 | const res = await fetch(url, { method }); 16 | 17 | const data = await res.json(); 18 | responseInputRef.current!.value = JSON.stringify(data, null, 2); 19 | } catch (error) { 20 | responseInputRef.current!.value = String(error); 21 | } 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 31 | 38 | 41 |
42 |