Here's the full flow, start to finish: --- 1. Startup (index.ts → commands/dev/index.ts) Running bun ../../index.ts dev: - init() is called twice (once in index.ts, once in the dev command — redundant but harmless). It ensures all required directories exist (src/pages/, .bunext/client/hydration-src/, public/pages/, etc.) and creates a blank bunext.config.ts if missing. - global.CONFIG is set with development: true. - global.ROUTER is created as a Bun.FileSystemRouter pointing at src/pages/ using Next.js-style routing. --- 2. Initial Build (start-server.ts → allPagesBundler) Before accepting requests, allPagesBundler() runs: 1. grabAllPages({ exclude_api: true }) — recursively walks src/pages/, skipping api/ routes and directories with ( or ) in the name, returning an array of { local_path, url_path, file_name }. 2. For each page, writeWebPageHydrationScript() generates a .tsx entrypoint in .bunext/client/hydration-src/pageName.tsx. That file looks like: import React from "react"; import { hydrateRoot } from "react-dom/client"; import App from "/absolute/path/to/src/pages/index.tsx"; const container = document.getElementById("bunext-root"); hydrateRoot(container, ); 3. Stale hydration files (for deleted pages) are cleaned up. 4. bundle() runs bun build .bunext/client/hydration-src/\*.tsx --outdir public/pages/ --minify via execSync. Bun bundles each entrypoint for the browser, outputting public/pages/pageName.js (and public/pages/pageName.css if any CSS was imported). --- 3. Server Start (server-params-gen.ts) Bun.serve() is called with a single fetch handler that routes by URL pathname: ┌─────────────────┬──────────────────────────┐ │ Path │ Handler │ ├─────────────────┼──────────────────────────┤ │ /\_\_hmr │ SSE stream for HMR │ ├─────────────────┼──────────────────────────┤ │ /api/_ │ handleRoutes │ ├─────────────────┼──────────────────────────┤ │ /public/_ │ Static file from public/ │ ├─────────────────┼──────────────────────────┤ │ /favicon.\* │ Static file from public/ │ ├─────────────────┼──────────────────────────┤ │ Everything else │ handleWebPages (SSR) │ └─────────────────┴──────────────────────────┘ --- 4. Incoming Page Request → handleWebPages 4a. Route matching (grab-page-component.tsx) - A new Bun.FileSystemRouter is created from src/pages/ (in dev; in prod it uses the cached global.ROUTER). - router.match(url.pathname) resolves the URL to an absolute file path (e.g. / → .../src/pages/index.tsx). - grabRouteParams() builds a BunxRouteParams object containing req, url, query (deserialized from search params), and body (parsed JSON for non-GET requests). 4b. Module import const module = await import(`${file_path}?t=${global.LAST_BUILD_TIME ?? 0}`); The ?t= cache-buster forces Bun to re-import the module after a rebuild instead of serving a stale cached version. 4c. server() function If the page exports a server function, it's called with routeParams and its return value becomes serverRes — the props passed to the component. This is the data-fetching layer (equivalent to Next.js getServerSideProps). 4d. Component instantiation const Component = module.default as FC; const component = ; The default export is treated as the page component, instantiated with the server-fetched props. ▎ If anything above throws (bad route, import error, etc.), the error path falls back to the /500 page (user-defined or the preset). --- 5. HTML Generation (generate-web-html.tsx) renderToString(component) is called — importing react-dom/server dynamically from process.cwd()/node_modules/ (the consumer's React, avoiding the duplicate-instance bug). The resulting HTML is assembled:
The pageProps (from server()) are serialized via EJSON and injected as window.**BUNEXT_PAGE_PROPS** so the client hydration script can read them without an extra network request. --- 6. Client Hydration (browser) The browser: 1. Parses the server-rendered HTML and displays it immediately (no blank flash). 2. Loads /public/pages/index.js (the Bun-bundled client script). 3. That script calls hydrateRoot(container, ) — React attaches event handlers to the existing DOM rather than re-rendering from scratch. At this point the page is fully interactive. --- 7. HMR (dev only) The injected EventSource("/\_\_hmr") maintains a persistent SSE connection. When the watcher detects a file change, it rebuilds all pages, updates LAST_BUILD_TIME, and sends event: update\ndata: reload down the SSE stream. The browser calls window.location.reload(), which re-requests the page and repeats steps 4–6 with the fresh module.