5.8 KiB
Here's the full flow, start to finish:
- 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.
- Initial Build (start-server.ts → allPagesBundler)
Before accepting requests, allPagesBundler() runs:
- 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 }.
- 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, <App {...window.BUNEXT_PAGE_PROPS} />);
- Stale hydration files (for deleted pages) are cleaned up.
- 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).
- 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) │ └─────────────────┴──────────────────────────┘
- 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 = <Component {...serverRes} />;
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).
- 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:
<html> <head> </head> </html>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.
- Client Hydration (browser)
The browser:
- Parses the server-rendered HTML and displays it immediately (no blank flash).
- Loads /public/pages/index.js (the Bun-bundled client script).
- That script calls hydrateRoot(container, <App {...window.BUNEXT_PAGE_PROPS} />) — React attaches event handlers to the existing DOM rather than re-rendering from scratch.
At this point the page is fully interactive.
- 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.