# Bunext A server-rendering framework for React, built entirely on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using ESBuild to bundle client assets and `Bun.serve` as the HTTP server. ## Philosophy Bunext is focused on **server-side rendering and processing**. Every page is rendered on the server on every request. The framework deliberately does not implement client-side navigation, SPA routing, or client-side state management — those concerns belong in client-side libraries and are orthogonal to what Bunext is solving. The goal is a framework that is: - Fast — Bun's runtime speed and ESBuild's bundling make the full dev loop snappy - Transparent — the entire request pipeline is readable and debugable - Standard — server functions and API handlers use native Web APIs (`Request`, `Response`, `URL`) with no custom wrappers --- ## Table of Contents - [Requirements](#requirements) - [Installation](#installation) - [Quick Start](#quick-start) - [CLI Commands](#cli-commands) - [Project Structure](#project-structure) - [File-System Routing](#file-system-routing) - [Non-Routed Directories](#non-routed-directories) - [Pages](#pages) - [Basic Page](#basic-page) - [Server Function](#server-function) - [Default Server Props](#default-server-props) - [Redirects from Server](#redirects-from-server) - [Custom Response Options](#custom-response-options) - [SEO Metadata](#seo-metadata) - [Dynamic Metadata](#dynamic-metadata) - [Custom Head Content](#custom-head-content) - [Root Layout](#root-layout) - [API Routes](#api-routes) - [Route Config (Body Size Limit)](#route-config-body-size-limit) - [Error Pages](#error-pages) - [Static Files](#static-files) - [Caching](#caching) - [Enabling Cache Per Page](#enabling-cache-per-page) - [Dynamic Cache Control from Server Function](#dynamic-cache-control-from-server-function) - [Cache Expiry](#cache-expiry) - [Cache Behavior and Limitations](#cache-behavior-and-limitations) - [Configuration](#configuration) - [Middleware](#middleware) - [WebSocket](#websocket) - [Server Options](#server-options) - [Custom Server](#custom-server) - [Environment Variables](#environment-variables) - [How It Works](#how-it-works) - [Development Server](#development-server) - [Production Build](#production-build) - [Bundler](#bundler) - [Hot Module Replacement](#hot-module-replacement) - [Request Pipeline](#request-pipeline) --- ## Requirements - [Bun](https://bun.sh) v1.0 or later - TypeScript 5.0+ > **React is managed by Bunext.** You do not need to install `react` or `react-dom` — Bunext enforces its own pinned React version and removes any user-installed copies at startup to prevent version conflicts. Installing this package is all you need. --- ## Installation ### From the Moduletrace registry (recommended) Configure the `@moduletrace` scope to point at the registry — pick one: **`bunfig.toml`** (Bun-native): ```toml [install.scopes] "@moduletrace" = { registry = "https://git.tben.me/api/packages/moduletrace/npm/" } ``` **`.npmrc`** (works with npm, bun, and most tools): ```ini @moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/ ``` Then install: ```bash bun add @moduletrace/bunext ``` Or globally: ```bash bun add -g @moduletrace/bunext ``` ### From GitHub (alternative) ```bash bun add github:moduletrace/bunext ``` --- ## Quick Start 1. Create a minimal project layout: ``` my-app/ ├── src/ │ └── pages/ │ └── index.tsx ├── bunext.config.ts # optional └── package.json ``` 2. Create your first page (`src/pages/index.tsx`): ```tsx export default function HomePage() { return

Hello from Bunext!

; } ``` 3. Add scripts to your `package.json`: ```json { "scripts": { "dev": "bunx bunext dev", "build": "bunx bunext build", "start": "bunx bunext start" } } ``` 4. Start the development server: ```bash bun run dev ``` 5. Open `http://localhost:7000` in your browser. --- ## CLI Commands | Command | Description | | -------------- | ---------------------------------------------------------------------- | | `bunext dev` | Start the development server with HMR and file watching. | | `bunext build` | Bundle all pages for production. Outputs artifacts to `.bunext/public/pages/`. | | `bunext start` | Start the production server using pre-built artifacts. | ### Running the CLI Bunext exposes a `bunext` binary. How you invoke it depends on how the package is installed: **Local install (recommended)** — add scripts to `package.json` and run them with `bun run`: ```json { "scripts": { "dev": "bunx bunext dev", "build": "bunx bunext build", "start": "bunx bunext start" } } ``` ```bash bun run dev bun run build bun run start ``` **Global install** — install once and use `bunext` from anywhere: ```bash bun add -g @moduletrace/bunext bunext dev bunext build bunext start ``` > **Note:** `bunext start` will exit with an error if `.bunext/public/pages/map.json` does not exist. Always run `bunext build` (or `bun run build`) before `bunext start`. --- ## Project Structure A typical Bunext project has the following layout: ``` my-app/ ├── src/ │ └── pages/ # File-system routes (pages and API handlers) │ ├── __root.tsx # Optional: root layout wrapper for all pages │ ├── __root.server.ts # Optional: root-level server logic │ ├── index.tsx # Route: / │ ├── index.server.ts # Optional: server logic for index route │ ├── about.tsx # Route: /about │ ├── 404.tsx # Optional: custom 404 page │ ├── 500.tsx # Optional: custom 500 page │ ├── blog/ │ │ ├── index.tsx # Route: /blog │ │ ├── index.server.ts # Server logic for /blog │ │ └── [slug].tsx # Route: /blog/:slug (dynamic) │ └── api/ │ └── users.ts # API route: /api/users ├── public/ # Static files served at /public/* ├── .bunext/ # Internal build artifacts (do not edit manually) │ └── public/ │ ├── pages/ # Generated by bundler │ │ └── map.json # Artifact map used by production server │ └── cache/ # File-based HTML cache (production only) ├── bunext.config.ts # Optional configuration ├── tsconfig.json └── package.json ``` --- ## File-System Routing Bunext uses `Bun.FileSystemRouter` with Next.js-style routing. Pages live in `src/pages/` and are automatically mapped to URL routes: | File path | URL path | | -------------------------------- | ------------- | | `src/pages/index.tsx` | `/` | | `src/pages/about.tsx` | `/about` | | `src/pages/blog/index.tsx` | `/blog` | | `src/pages/blog/[slug].tsx` | `/blog/:slug` | | `src/pages/users/[id]/index.tsx` | `/users/:id` | | `src/pages/api/users.ts` | `/api/users` | Dynamic route parameters (e.g. `[slug]`) are available in the `server` function via `ctx.req.url` or from the `query` field in the server response. ### Non-Routed Directories Directories whose name contains `--` or a parenthesis (`(` or `)`) are completely ignored by the router. Use this to co-locate helper components, utilities, or shared logic directly inside `src/pages/` alongside the routes that use them, without them becoming routes. | Naming pattern | Effect | | --------------- | -------------------- | | `(components)/` | Ignored — not routed | | `--utils--/` | Ignored — not routed | | `--lib/` | Ignored — not routed | ``` src/pages/ ├── blog/ │ ├── (components)/ # Not a route — co-location directory │ │ ├── PostCard.tsx # Used by index.tsx and [slug].tsx │ │ └── PostList.tsx │ ├── index.tsx # Route: /blog │ └── [slug].tsx # Route: /blog/:slug └── index.tsx # Route: / ``` --- ## Pages ### Basic Page A page file must export a default React component. The component receives server-side props automatically. ```tsx // src/pages/index.tsx export default function HomePage() { return

Hello, World!

; } ``` ### Server Function Server logic lives in a companion **`.server.ts`** (or `.server.tsx`) file alongside the page. The framework looks for `.server.ts` or `.server.tsx` next to the page file and loads it separately on the server — it is never bundled into the client JS. The server file exports the server function as either `export default` or `export const server`. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters. ```ts // src/pages/profile.server.ts import type { BunextPageServerFn } from "@moduletrace/bunext/types"; const server: BunextPageServerFn<{ username: string; bio: string; }> = async (ctx) => { // ctx.req — the raw Request object // ctx.url — the parsed URL // ctx.query — query string parameters // ctx.resTransform — optional response interceptor const username = "alice"; const bio = "Software engineer"; return { props: { username, bio }, }; }; export default server; ``` ```tsx // src/pages/profile.tsx (client-only exports — bundled to the browser) type Props = { props?: { username: string; bio: string }; query?: Record; url?: URL; }; export default function ProfilePage({ props, url }: Props) { return (

{props?.username}

{props?.bio}

Current path: {url?.pathname}

); } ``` > **Why separate files?** Bundling server code (Bun APIs, database clients, `fs`, secrets) into the same file as a React component causes TypeScript compilation errors because the bundler processes the page file for the browser. The `.server.ts` companion file is loaded only by the server at request time and is never included in the client bundle. The server function receives a `ctx` object (type `BunxRouteParams`) with: | Field | Type | Description | | -------------- | ------------------------------------------------ | ------------------------------------------ | | `req` | `Request` | Raw Bun/Web Request object | | `url` | `URL` | Parsed URL | | `body` | `any` | Parsed request body | | `query` | `any` | Query string parameters | | `resTransform` | `(res: Response) => Promise\|Response` | Intercept and transform the final response | ### Default Server Props Every page component automatically receives a `url` prop — a copy of the request URL object — even if no `server` function is exported. This means you can always read URL data (pathname, search params, origin, etc.) directly from component props without writing a `server` function. ```tsx // src/pages/index.tsx import type { BunextPageModuleServerReturnURLObject } from "@moduletrace/bunext/types"; type Props = { url?: BunextPageModuleServerReturnURLObject; }; export default function HomePage({ url }: Props) { return (

Visiting: {url?.pathname}

Origin: {url?.origin}

); } ``` The `url` prop exposes the following fields from the standard Web `URL` interface: | Field | Type | Example | | -------------- | ----------------- | -------------------------------- | | `href` | `string` | `"https://example.com/blog?q=1"` | | `origin` | `string` | `"https://example.com"` | | `protocol` | `string` | `"https:"` | | `host` | `string` | `"example.com"` | | `hostname` | `string` | `"example.com"` | | `port` | `string` | `""` | | `pathname` | `string` | `"/blog"` | | `search` | `string` | `"?q=1"` | | `searchParams` | `URLSearchParams` | `URLSearchParams { q: "1" }` | | `hash` | `string` | `""` | | `username` | `string` | `""` | | `password` | `string` | `""` | ### Redirects from Server Return a `redirect` object from the server function to redirect the client: ```ts // src/pages/dashboard.server.ts import type { BunextPageServerFn } from "@moduletrace/bunext/types"; const server: BunextPageServerFn = async (ctx) => { const isLoggedIn = false; // check auth if (!isLoggedIn) { return { redirect: { destination: "/login", permanent: false, // uses 302 // status_code: 307 // override status code }, }; } return { props: {} }; }; export default server; ``` `permanent: true` sends a `301` redirect. Otherwise it sends `302`, or the value of `status_code` if provided. ### Custom Response Options Control status codes, headers, and other response options from the server function: ```ts // src/pages/submit.server.ts import type { BunextPageServerFn } from "@moduletrace/bunext/types"; const server: BunextPageServerFn = async (ctx) => { return { props: { message: "Created" }, responseOptions: { status: 201, headers: { "X-Custom-Header": "my-value", }, }, }; }; export default server; ``` ### SEO Metadata Export a `meta` object from the **page file** (not the server file) to inject SEO and Open Graph tags into the ``: ```tsx import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; export const meta: BunextPageModuleMeta = { title: "My Page Title", description: "A description for search engines.", keywords: ["bun", "react", "ssr"], author: "Alice", robots: "index, follow", canonical: "https://example.com/about", themeColor: "#ffffff", og: { title: "My Page Title", description: "Shared on social media.", image: "https://example.com/og-image.png", url: "https://example.com/about", type: "website", siteName: "My Site", locale: "en_US", }, twitter: { card: "summary_large_image", title: "My Page Title", description: "Shared on Twitter.", image: "https://example.com/twitter-image.png", site: "@mysite", creator: "@alice", }, }; export default function AboutPage() { return

About us

; } ``` ### Dynamic Metadata `meta` can also be an async function that receives the request context and server response. Like `meta`, it is exported from the **page file**: ```tsx import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types"; export const meta: BunextPageModuleMetaFn = async ({ ctx, serverRes }) => { return { title: `Post: ${serverRes?.props?.title ?? "Untitled"}`, description: serverRes?.props?.excerpt, }; }; ``` ### Custom Head Content Export a `Head` functional component to inject arbitrary HTML into ``. It receives the server response and request context: ```tsx import type { BunextPageHeadFCProps } from "@moduletrace/bunext/types"; export function Head({ serverRes, ctx }: BunextPageHeadFCProps) { return ( <> ); } export default function Page() { return
Content
; } ``` ### Root Layout Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props. If the root layout also needs server-side logic, place it in `src/pages/__root.server.ts` (or `.server.tsx`) — the same `.server.*` convention used by regular pages: ```ts // src/pages/__root.server.ts import type { BunextPageServerFn } from "@moduletrace/bunext/types"; const server: BunextPageServerFn = async (ctx) => { // e.g. fetch navigation links, check auth return { props: { navLinks: ["/", "/about"] } }; }; export default server; ``` ```tsx // src/pages/__root.tsx import type { BunextRootComponentProps } from "@moduletrace/bunext/types"; export default function RootLayout({ children, props, }: BunextRootComponentProps) { return ( <>
My App
{children}
© 2025
); } ``` --- ## API Routes Create files under `src/pages/api/` to define API endpoints. The default export receives a `BunxRouteParams` object and must return a standard `Response`. ```ts // src/pages/api/hello.ts import type { BunxRouteParams } from "@moduletrace/bunext/types"; export default async function handler(ctx: BunxRouteParams): Promise { return Response.json({ message: "Hello from the API" }); } ``` API routes are matched at `/api/`. Because the handler returns a plain `Response`, you control the status code, headers, and body format entirely: ```ts // src/pages/api/users.ts import type { BunxRouteParams } from "@moduletrace/bunext/types"; export default async function handler(ctx: BunxRouteParams): Promise { if (ctx.req.method !== "GET") { return Response.json({ error: "Method not allowed" }, { status: 405 }); } const users = [{ id: 1, name: "Alice" }]; return Response.json({ success: true, data: users }); } ``` The `ctx` parameter has the same shape as the page `server` function context — see the [Server Function](#server-function) section for the full field reference. The `ctx.server` field additionally exposes the Bun `Server` instance. ### Route Config (Body Size Limit) Export a `config` object to override the per-route request body limit (default: 10 MB): ```ts import type { BunextServerRouteConfig, BunxRouteParams, } from "@moduletrace/bunext/types"; export const config: BunextServerRouteConfig = { maxRequestBodyMB: 50, // allow up to 50 MB }; export default async function handler(ctx: BunxRouteParams): Promise { // handle large uploads return Response.json({ success: true }); } ``` --- ## Error Pages Bunext serves built-in fallback error pages, but you can override them by creating pages in `src/pages/`: | File | Triggered when | | ------------------- | --------------------------------------- | | `src/pages/404.tsx` | No matching route is found | | `src/pages/500.tsx` | An unhandled error occurs during render | The error message is passed as `children` to the component: ```tsx // src/pages/404.tsx import type { PropsWithChildren } from "react"; export default function NotFoundPage({ children }: PropsWithChildren) { return (

404 — Page Not Found

{children}

); } ``` If no custom error pages exist, Bunext uses built-in preset components. --- ## Static Files Place static files in the `public/` directory. They are served at the `/public/` URL path: ``` public/ ├── logo.png → http://localhost:7000/public/logo.png ├── styles.css → http://localhost:7000/public/styles.css └── favicon.ico → http://localhost:7000/favicon.ico ``` > Favicons are also served from `/public/` but matched directly at `/favicon.*`. --- ## Caching Bunext includes a file-based HTML cache for production. Caching is **disabled in development** — every request renders fresh. In production, a cron job runs every 30 seconds to delete expired cache entries. Cache files are stored in `.bunext/public/cache/`. Each cached page produces two files: | File | Contents | | ----------------- | ---------------------------------------------- | | `.res.html` | The cached HTML response body | | `.meta.json` | Metadata: creation timestamp, expiry, paradigm | The cache is **cold on first request**: the first visitor triggers a full server render and writes the cache. Every subsequent request within the expiry window receives the cached HTML directly, bypassing the server function, component render, and bundler lookup. A cache hit is indicated by the response header `X-Bunext-Cache: HIT`. ### Enabling Cache Per Page Export a `config` object from a page file to opt that page into caching: ```tsx // src/pages/products.tsx import type { BunextRouteConfig } from "@moduletrace/bunext/types"; export const config: BunextRouteConfig = { cachePage: true, cacheExpiry: 300, // seconds — optional, overrides the global default }; export default function ProductsPage() { return

Products

; } ``` ### Dynamic Cache Control from Server Function Cache settings can also be returned from the server function, which lets you conditionally enable caching based on request data: ```ts // src/pages/products.server.ts import type { BunextPageServerFn } from "@moduletrace/bunext/types"; const server: BunextPageServerFn = async (ctx) => { const data = await fetchProducts(); return { props: { data }, cachePage: true, cacheExpiry: 600, // 10 minutes }; }; export default server; ``` ```tsx // src/pages/products.tsx export default function ProductsPage({ props }: any) { return (
    {props.data.map((p: any) => (
  • {p.name}
  • ))}
); } ``` If both `module.config.cachePage` and `serverRes.cachePage` are set, `module.config` takes precedence. ### Cache Expiry Expiry resolution order (first truthy value wins): 1. `cacheExpiry` on the page `config` export or `server` return (per-page, in seconds) 2. `defaultCacheExpiry` in `bunext.config.ts` (global default, in seconds) 3. Built-in default: **3600 seconds (1 hour)** The cron job checks all cache entries every 30 seconds and deletes any whose age exceeds their expiry. Static bundled assets (JS/CSS in `.bunext/public/`) receive a separate HTTP `Cache-Control: public, max-age=604800` header (7 days) via the browser cache — this is independent of the page HTML cache. ### Cache Behavior and Limitations - **Production only.** Caching never activates in development (`bunext dev`). - **Cold start required.** The cache is populated on the first request; there is no pre-warming step. - **Immutable within the expiry window.** Once a page is cached, `writeCache` skips all subsequent write attempts for that key until the cron job deletes the expired entry. There is no manual invalidation API. - **Cache is not cleared on rebuild.** Deploying a new build does not automatically flush `.bunext/public/cache/`. Stale HTML files referencing old JS bundles can be served until they expire. Clear the cache directory as part of your deploy process if needed. - **No key collision.** Cache keys are generated via `encodeURIComponent()` on the URL path. `/foo/bar` encodes to `%2Ffoo%2Fbar` and `/foo-bar` to `%2Ffoo-bar` — distinct filenames with no collision risk. --- ## Configuration Create a `bunext.config.ts` file in your project root to configure Bunext: ```ts // bunext.config.ts import type { BunextConfig } from "@moduletrace/bunext/types"; const config: BunextConfig = { port: 3000, // default: 7000 origin: "https://example.com", distDir: ".bunext", // directory for internal build artifacts assetsPrefix: "_bunext/static", globalVars: { MY_API_URL: "https://api.example.com", }, development: false, // forced by the CLI; set manually if needed }; export default config; ``` | Option | Type | Default | Description | | -------------------- | --------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------- | | `port` | `number` | `7000` | HTTP server port | | `origin` | `string` | — | Canonical origin URL | | `distDir` | `string` | `.bunext` | Internal artifact directory | | `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets | | `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time | | `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically | | `defaultCacheExpiry` | `number` | `3600` | Global page cache expiry in seconds | | `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) | | `websocket` | `WebSocketHandler` | — | Bun WebSocket handler — see [WebSocket](#websocket) | | `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) | ### Middleware Middleware runs on every request before any routing. Define it in `bunext.config.ts` via the `middleware` field. The function receives `{ req, url, server }` (type `BunextConfigMiddlewareParams`). - If it returns a `Response`, that response is sent to the client immediately and no further routing occurs. - If it returns `undefined` (or nothing), the request proceeds normally through the router. ```ts // bunext.config.ts import type { BunextConfig, BunextConfigMiddlewareParams, } from "@moduletrace/bunext/types"; const config: BunextConfig = { middleware: async ({ req, url }) => { // Example: protect all /dashboard/* routes if (url.pathname.startsWith("/dashboard")) { const token = req.headers.get("authorization"); if (!token) { return Response.redirect("/login", 302); } } // Return undefined to continue to the normal request pipeline return undefined; }, }; export default config; ``` The middleware can return any valid `Response`, including redirects, JSON, or HTML: ```ts middleware: async ({ req, url }) => { // Block a specific path if (url.pathname === "/maintenance") { return new Response("Down for maintenance", { status: 503 }); } // For API routes, return undefined to continue — the handler controls its own Response directly }, ``` ### WebSocket Add a `websocket` field to `bunext.config.ts` to handle WebSocket connections. The value is passed directly to `Bun.serve()` as the `websocket` option and accepts the full [`WebSocketHandler`](https://bun.sh/docs/api/websockets) interface. Define the handler in its own file and import it into the config: ```ts // websocket.ts import type { WebSocketHandler } from "bun"; export const BunextWebsocket: WebSocketHandler = { message(ws, message) { console.log(`WS Message => ${message}`); }, }; ``` ```ts // bunext.config.ts import type { BunextConfig } from "@moduletrace/bunext/types"; import { BunextWebsocket } from "./websocket"; const config: BunextConfig = { websocket: BunextWebsocket, }; export default config; ``` ### Server Options Pass additional options to the underlying `Bun.serve()` call via `serverOptions`. All standard [`ServeOptions`](https://bun.sh/docs/api/http) fields are accepted except `fetch`, which Bunext manages internally. ```ts // bunext.config.ts import type { BunextConfig } from "@moduletrace/bunext/types"; const config: BunextConfig = { serverOptions: { tls: { cert: Bun.file("./certs/cert.pem"), key: Bun.file("./certs/key.pem"), }, maxRequestBodySize: 64 * 1024 * 1024, // 64 MB error(err) { console.error("Server error:", err); return new Response("Internal Server Error", { status: 500 }); }, }, }; export default config; ``` --- ## Custom Server For full control over the `Bun.serve()` instance — custom WebSocket upgrade logic, multi-protocol handling, or integrating Bunext alongside other handlers — you can skip `bunext dev` / `bunext start` and run your own server using Bunext's exported primitives. ```ts // server.ts import bunext from "@moduletrace/bunext"; const development = process.env.NODE_ENV === "development"; const port = process.env.PORT || 3700; await bunext.bunextInit(); const server = Bun.serve({ routes: { "/*": { async GET(req) { return await bunext.bunextRequestHandler({ req }); }, }, }, development, port, }); bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`); ``` | Export | Type | Description | | ------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `bunextInit()` | `() => Promise` | Initializes config, router, and bundler. Must be called before handling requests. | | `bunextRequestHandler({ req })` | `(params: { req: Request }) => Promise` | The main Bunext request dispatcher — middleware, routing, SSR, static files. Only `req` is needed; the server instance is managed internally. | | `bunextLog` | Logger | Framework logger (`info`, `error`, `success`, `server`, `watch`). | Run the custom server directly with Bun: ```bash bun run server.ts ``` > **Note:** When using a custom server, HMR and file watching are still driven by `bunextInit()`. Pass `development: true` in your `Bun.serve()` call to enable them. --- ## Environment Variables | Variable | Description | | -------- | ------------------------------------------------------- | | `PORT` | Override the server port (takes precedence over config) | --- ## How It Works ### Development Server Running `bunext dev`: 1. Loads `bunext.config.ts` and sets `development: true`. 2. Initializes directories (`.bunext/`, `public/pages/`). 3. Creates a `Bun.FileSystemRouter` pointed at `src/pages/`. 4. Creates an ESBuild context and performs the initial build. File-change rebuilds are triggered manually by the FS watcher. 5. Starts a file-system watcher on `src/` — when a file is created or deleted (a "rename" event), it triggers a full bundler rebuild to update the entry points. 6. Waits for the first successful bundle. 7. Starts `Bun.serve()`. ### Production Build Running `bunext build`: 1. Sets `NODE_ENV=production`. 2. Runs ESBuild once with minification enabled. 3. Writes all bundled artifacts to `.bunext/public/pages/` and the artifact map to `.bunext/public/pages/map.json`. 4. Exits. ### Production Server Running `bunext start`: 1. Reads `.bunext/public/pages/map.json` to load the pre-built artifact map. 2. Starts `Bun.serve()` without any bundler or file watcher. ### Bundler The bundler uses **ESBuild** with a virtual namespace plugin that generates in-memory hydration entry points for each page — no temporary files are written to disk. Each virtual entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout. Tailwind CSS is processed via a dedicated ESBuild plugin. In development, an `esbuild.context()` is created once and rebuilt incrementally whenever the FS watcher detects a file change. In production, a single `esbuild.build()` call runs with minification enabled. React is loaded externally — `react`, `react-dom`, `react-dom/client`, and `react/jsx-runtime` are all marked as external in the ESBuild config. The correct React version is resolved from the framework's own `node_modules` at startup and injected into every HTML page via a `