From 0cea20deb99d8794c6c311c6b28347a79a3ed35f Mon Sep 17 00:00:00 2001 From: Javi Aguilar <122741+itsjavi@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:04:19 +0100 Subject: [PATCH 1/2] feat: add Bun template --- .gitignore | 2 + bun/.dockerignore | 4 + bun/.gitignore | 7 ++ bun/Dockerfile | 23 +++++ bun/README.md | 86 +++++++++++++++++++ bun/app/app.css | 15 ++++ bun/app/entry.client.tsx | 12 +++ bun/app/entry.server.tsx | 53 ++++++++++++ bun/app/root.tsx | 75 ++++++++++++++++ bun/app/routes.ts | 3 + bun/app/routes/home.tsx | 13 +++ bun/app/welcome/logo-dark.svg | 23 +++++ bun/app/welcome/logo-light.svg | 23 +++++ bun/app/welcome/welcome.tsx | 89 +++++++++++++++++++ bun/package.json | 28 ++++++ bun/public/favicon.ico | Bin 0 -> 15086 bytes bun/react-router.config.ts | 7 ++ bun/server.ts | 152 +++++++++++++++++++++++++++++++++ bun/tsconfig.json | 27 ++++++ bun/vite.config.ts | 8 ++ pnpm-lock.yaml | 70 ++++++++++++++- pnpm-workspace.yaml | 1 + 22 files changed, 719 insertions(+), 2 deletions(-) create mode 100644 bun/.dockerignore create mode 100644 bun/.gitignore create mode 100644 bun/Dockerfile create mode 100644 bun/README.md create mode 100644 bun/app/app.css create mode 100644 bun/app/entry.client.tsx create mode 100644 bun/app/entry.server.tsx create mode 100644 bun/app/root.tsx create mode 100644 bun/app/routes.ts create mode 100644 bun/app/routes/home.tsx create mode 100644 bun/app/welcome/logo-dark.svg create mode 100644 bun/app/welcome/logo-light.svg create mode 100644 bun/app/welcome/welcome.tsx create mode 100644 bun/package.json create mode 100644 bun/public/favicon.ico create mode 100644 bun/react-router.config.ts create mode 100644 bun/server.ts create mode 100644 bun/tsconfig.json create mode 100644 bun/vite.config.ts diff --git a/.gitignore b/.gitignore index a3e300f..b02b575 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ .parcel-cache # Deno deno.lock +# Bun +bun.lock diff --git a/bun/.dockerignore b/bun/.dockerignore new file mode 100644 index 0000000..9b8d514 --- /dev/null +++ b/bun/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md \ No newline at end of file diff --git a/bun/.gitignore b/bun/.gitignore new file mode 100644 index 0000000..039ee62 --- /dev/null +++ b/bun/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.env +/node_modules/ + +# React Router +/.react-router/ +/build/ diff --git a/bun/Dockerfile b/bun/Dockerfile new file mode 100644 index 0000000..7d7364a --- /dev/null +++ b/bun/Dockerfile @@ -0,0 +1,23 @@ +FROM oven/bun:1-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN bun ci + +FROM oven/bun:1-alpine AS production-dependencies-env +COPY ./package.json bun.lock /app/ +WORKDIR /app +RUN bun ci --omit=dev + +FROM oven/bun:1-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN bun run build + +FROM oven/bun:1-alpine +COPY ./package.json bun.lock /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +COPY --from=build-env /app/server.ts /app/server.ts +WORKDIR /app +CMD ["bun", "--trace-uncaught", "--trace-warnings", "run", "/app/server.ts", "build/server/index.js"] diff --git a/bun/README.md b/bun/README.md new file mode 100644 index 0000000..b1cb0c8 --- /dev/null +++ b/bun/README.md @@ -0,0 +1,86 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router and Bun. + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +bun install +``` + +### Development + +Start the development server with HMR: + +```bash +bun run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +bun run build +``` + +## Deployment + +### Docker Deployment + +To build and run using Docker: + +```bash +docker build -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Bun applications, the built-in app server is production-ready, even though it is +still recommended to use a dedicated static file server for production, e.g. nginx, with gzip compression enabled. + +Make sure to deploy the output of `bun run build` + +``` +├── package.json +├── bun.lock +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/bun/app/app.css b/bun/app/app.css new file mode 100644 index 0000000..99345d8 --- /dev/null +++ b/bun/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/bun/app/entry.client.tsx b/bun/app/entry.client.tsx new file mode 100644 index 0000000..7e8a937 --- /dev/null +++ b/bun/app/entry.client.tsx @@ -0,0 +1,12 @@ +import { startTransition, StrictMode } from 'react' +import { hydrateRoot } from 'react-dom/client' +import { HydratedRouter } from 'react-router/dom' + +startTransition(() => { + hydrateRoot( + document, + + + , + ) +}) diff --git a/bun/app/entry.server.tsx b/bun/app/entry.server.tsx new file mode 100644 index 0000000..bd63a21 --- /dev/null +++ b/bun/app/entry.server.tsx @@ -0,0 +1,53 @@ +import { isbot } from 'isbot' +import { renderToReadableStream } from 'react-dom/server' +import type { EntryContext, HandleErrorFunction } from 'react-router' +import { ServerRouter } from 'react-router' + +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('Streaming rendering 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, + }) +} + +/** + * This function is called whenever React Router catches an error in your application on the server. + * @see https://reactrouter.com/how-to/error-reporting + */ +export const handleError: HandleErrorFunction = (error: unknown, { request }: { request: Request }) => { + if (request.signal.aborted) { + // React Router may abort some interrupted requests, don't log those as errors + return + } + console.error('Uncaught Error', error) +} diff --git a/bun/app/root.tsx b/bun/app/root.tsx new file mode 100644 index 0000000..9fc6636 --- /dev/null +++ b/bun/app/root.tsx @@ -0,0 +1,75 @@ +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +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", + }, +]; + +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/bun/app/routes.ts b/bun/app/routes.ts new file mode 100644 index 0000000..102b402 --- /dev/null +++ b/bun/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/bun/app/routes/home.tsx b/bun/app/routes/home.tsx new file mode 100644 index 0000000..398e47c --- /dev/null +++ b/bun/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/bun/app/welcome/logo-dark.svg b/bun/app/welcome/logo-dark.svg new file mode 100644 index 0000000..dd82028 --- /dev/null +++ b/bun/app/welcome/logo-dark.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bun/app/welcome/logo-light.svg b/bun/app/welcome/logo-light.svg new file mode 100644 index 0000000..7328492 --- /dev/null +++ b/bun/app/welcome/logo-light.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bun/app/welcome/welcome.tsx b/bun/app/welcome/welcome.tsx new file mode 100644 index 0000000..8ac6e1d --- /dev/null +++ b/bun/app/welcome/welcome.tsx @@ -0,0 +1,89 @@ +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/bun/package.json b/bun/package.json new file mode 100644 index 0000000..fb0c777 --- /dev/null +++ b/bun/package.json @@ -0,0 +1,28 @@ +{ + "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": { + "isbot": "^5.1.31", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router": "^7.9.2" + }, + "devDependencies": { + "@react-router/dev": "^7.9.2", + "@tailwindcss/vite": "^4.1.13", + "@types/bun": "^1.3.1", + "@types/node": "^22", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "tailwindcss": "^4.1.13", + "typescript": "^5.9.2", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4" + } +} \ No newline at end of file diff --git a/bun/public/favicon.ico b/bun/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5dbdfcddcb14182535f6d32d1c900681321b1aa3 GIT binary patch literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j literal 0 HcmV?d00001 diff --git a/bun/react-router.config.ts b/bun/react-router.config.ts new file mode 100644 index 0000000..6ff16f9 --- /dev/null +++ b/bun/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/bun/server.ts b/bun/server.ts new file mode 100644 index 0000000..3f8ee85 --- /dev/null +++ b/bun/server.ts @@ -0,0 +1,152 @@ +import type { Server } from 'bun' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import url from 'node:url' +import type { ServerBuild } from 'react-router' +import { createRequestHandler } from 'react-router' + +type RSCServerBuild = { + fetch: (request: Request, client?: { + address: string + family: 'IPv4' | 'IPv6' + port: number + }) => Response + publicPath: string + assetsBuildDirectory: string +} + +type IsomorphicServerBuild = (RSCServerBuild & { isRsc: true }) | (ServerBuild & { isRsc: false }) + +async function resolveBuildModule() { + const buildPathArg = process.argv[process.argv.length - 1] ?? undefined + if (!buildPathArg) { + console.error(`Usage example: bun run server.ts build/server/index.js`) + process.exit(1) + } + const buildPath = path.resolve(buildPathArg) + const buildModule = await import(url.pathToFileURL(buildPath).href) + return { buildPath, buildModule } +} + +async function prepareServer(buildPath: string, buildModule: any) { + let port = parseNumber(process.env.PORT) ?? 3000 + let build: IsomorphicServerBuild + + if (buildModule.default && typeof buildModule.default === 'function') { + const config = { + publicPath: '/', + assetsBuildDirectory: '../client', + ...(buildModule.unstable_reactRouterServeConfig || {}), + } + build = { + fetch: buildModule.default, + publicPath: config.publicPath, + assetsBuildDirectory: path.resolve(path.dirname(buildPath), config.assetsBuildDirectory), + isRsc: true, + } satisfies IsomorphicServerBuild + } else { + build = { + ...(buildModule as ServerBuild), + isRsc: false, + } satisfies IsomorphicServerBuild + } + + return { port, build } +} + +function runBunServer(build: IsomorphicServerBuild, port: number) { + // It is recommended to use a dedicated static file server for production, e.g. nginx, with gzip compression enabled + // This is a simple fallback using Bun to stream static files, but it's not serving them compressed. + const handleStaticFiles = (req: Request): Response | null => { + const filePath = new URL(req.url).pathname + if (!filePath.startsWith(build.publicPath)) { + return null + } + + const safeFilePath = filePath.replace(/^\/+/, '').replace(/\.\./g, '').replace(/\/\//, '/') + const fullPath = path.join(build.assetsBuildDirectory, safeFilePath) + + if (fs.existsSync(fullPath) && !fs.statSync(fullPath).isDirectory()) { + const file = Bun.file(fullPath) + return new Response(file, { + headers: { + 'Content-Type': file.type, + 'Cache-Control': 'public, max-age=31536000, immutable', // 1 year + }, + }) + } + + return null + } + + const server: Server = Bun.serve({ + port, + // reusePort: true, // uncomment to allow multiple instances to listen on the same port (e.g. for clustering) + development: process.env.NODE_ENV !== 'production', + fetch: async (req) => { + const startTime = Date.now() + const staticFile = handleStaticFiles(req) + if (staticFile) { + return staticFile + } + + let response: Response + try { + if (build.isRsc) { + const remote = server.requestIP(req) + response = build.fetch(req, remote ?? undefined) + if (typeof (response as any).then === 'function') { + response = await response + } + } else { + response = await createRequestHandler(build, process.env.NODE_ENV)(req) + } + } catch (error: unknown) { + console.error('Uncaught error', error) + response = new Response('Internal Server Error', { status: 500 }) + } + + // Add the total server processing time header to the response. + // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing + const serverTiming = Date.now() - startTime + response.headers.set('Server-Timing', `total;dur=${serverTiming}`) + logRequest(req, response, serverTiming) + return response + }, + }) + + ;['SIGTERM', 'SIGINT'].forEach((signal) => { + process.once(signal, () => server?.stop()) + }) + + return server +} + +function parseNumber(raw?: string | null) { + if (raw === undefined || raw === null) return undefined + let maybe = Number(raw) + if (Number.isNaN(maybe)) return undefined + return maybe +} + +function logRequest(req: Request, response: Response, serverTiming: number): number { + const pathname = new URL(req.url).pathname + const statusCode = response.status + + console.log(`${req.method.toUpperCase()} ${pathname} - HTTP ${statusCode} - ${serverTiming}ms`) + + return serverTiming +} + +async function main() { + const buildModule = await resolveBuildModule() + const { port, build } = await prepareServer(buildModule.buildPath, buildModule.buildModule) + if (!build.publicPath || !build.assetsBuildDirectory) { + throw new Error(`${buildModule.buildPath} is not a valid build file.`) + } + const server = runBunServer(build, port) + console.log(`[Bun Server] Listening on ${server.url}`) +} + +main() diff --git a/bun/tsconfig.json b/bun/tsconfig.json new file mode 100644 index 0000000..dc391a4 --- /dev/null +++ b/bun/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/bun/vite.config.ts b/bun/vite.config.ts new file mode 100644 index 0000000..4a88d58 --- /dev/null +++ b/bun/vite.config.ts @@ -0,0 +1,8 @@ +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()], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a16122e..a3b7d4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,52 @@ importers: specifier: ^2.0.3 version: 2.0.3 + bun: + dependencies: + isbot: + specifier: ^5.1.31 + version: 5.1.31 + react: + specifier: ^19.1.1 + version: 19.1.1 + react-dom: + specifier: ^19.1.1 + version: 19.1.1(react@19.1.1) + react-router: + specifier: ^7.9.2 + version: 7.9.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + devDependencies: + '@react-router/dev': + specifier: ^7.9.2 + version: 7.9.2(@react-router/serve@7.9.2(react-router@7.9.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.2))(@types/node@22.15.3)(@vitejs/plugin-rsc@0.4.31(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)(yaml@2.6.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(react-router@7.9.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(tsx@4.19.2)(typescript@5.9.2)(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)(yaml@2.6.1))(wrangler@4.40.0)(yaml@2.6.1) + '@tailwindcss/vite': + specifier: ^4.1.13 + version: 4.1.13(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)(yaml@2.6.1)) + '@types/bun': + specifier: ^1.3.1 + version: 1.3.2(@types/react@19.1.13) + '@types/node': + specifier: ^22 + version: 22.15.3 + '@types/react': + specifier: ^19.1.13 + version: 19.1.13 + '@types/react-dom': + specifier: ^19.1.9 + version: 19.1.9(@types/react@19.1.13) + tailwindcss: + specifier: ^4.1.13 + version: 4.1.13 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)(yaml@2.6.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.15.3)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.19.2)(yaml@2.6.1)) + default: dependencies: '@react-router/node': @@ -248,7 +294,7 @@ importers: version: 1.8.1 drizzle-orm: specifier: ~0.36.3 - version: 0.36.3(@cloudflare/workers-types@4.20250429.0)(@opentelemetry/api@1.8.0)(@types/pg@8.11.14)(@types/react@19.1.13)(postgres@3.4.5)(react@19.1.1) + version: 0.36.3(@cloudflare/workers-types@4.20250429.0)(@opentelemetry/api@1.8.0)(@types/pg@8.11.14)(@types/react@19.1.13)(bun-types@1.3.2(@types/react@19.1.13))(postgres@3.4.5)(react@19.1.1) express: specifier: ^5.1.0 version: 5.1.0 @@ -2289,6 +2335,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/bun@1.3.2': + resolution: {integrity: sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg==} + '@types/compression@1.8.1': resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} @@ -2459,6 +2508,11 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bun-types@1.3.2: + resolution: {integrity: sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg==} + peerDependencies: + '@types/react': ^19 + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -6092,6 +6146,12 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.15.3 + '@types/bun@1.3.2(@types/react@19.1.13)': + dependencies: + bun-types: 1.3.2(@types/react@19.1.13) + transitivePeerDependencies: + - '@types/react' + '@types/compression@1.8.1': dependencies: '@types/express': 5.0.3 @@ -6374,6 +6434,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bun-types@1.3.2(@types/react@19.1.13): + dependencies: + '@types/node': 22.15.3 + '@types/react': 19.1.13 + bytes@3.1.2: {} cac@6.7.14: {} @@ -6541,12 +6606,13 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.36.3(@cloudflare/workers-types@4.20250429.0)(@opentelemetry/api@1.8.0)(@types/pg@8.11.14)(@types/react@19.1.13)(postgres@3.4.5)(react@19.1.1): + drizzle-orm@0.36.3(@cloudflare/workers-types@4.20250429.0)(@opentelemetry/api@1.8.0)(@types/pg@8.11.14)(@types/react@19.1.13)(bun-types@1.3.2(@types/react@19.1.13))(postgres@3.4.5)(react@19.1.1): optionalDependencies: '@cloudflare/workers-types': 4.20250429.0 '@opentelemetry/api': 1.8.0 '@types/pg': 8.11.14 '@types/react': 19.1.13 + bun-types: 1.3.2(@types/react@19.1.13) postgres: 3.4.5 react: 19.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index de00ace..e7ea995 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - ".tests" + - "bun" - "default" - "javascript" - "minimal" From 3028315279a074d875aa8f2895a9a041ad85637b Mon Sep 17 00:00:00 2001 From: Javi Aguilar <122741+itsjavi@users.noreply.github.com> Date: Sat, 8 Nov 2025 23:34:48 +0100 Subject: [PATCH 2/2] improve DIY deployment instructions --- bun/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bun/README.md b/bun/README.md index b1cb0c8..a6753fa 100644 --- a/bun/README.md +++ b/bun/README.md @@ -72,11 +72,14 @@ Make sure to deploy the output of `bun run build` ``` ├── package.json ├── bun.lock +├── server.ts ├── build/ │ ├── client/ # Static assets │ └── server/ # Server-side code ``` +Then, run `bun run server.ts build/server/index.js` + ## Styling This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.