Updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Bunext
|
||||
|
||||
A server-rendering framework for React, built 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.
|
||||
A server-rendering framework for React, built entirely on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using `Bun.build` to bundle client assets and `Bun.serve` as the HTTP server.
|
||||
|
||||
## Philosophy
|
||||
|
||||
@@ -8,7 +8,7 @@ Bunext is focused on **server-side rendering and processing**. Every page is ren
|
||||
|
||||
The goal is a framework that is:
|
||||
|
||||
- Fast — Bun's runtime speed and ESBuild's bundling make the full dev loop snappy
|
||||
- Fast — Bun's runtime speed and Bun.build'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
|
||||
|
||||
@@ -61,7 +61,8 @@ The goal is a framework that is:
|
||||
|
||||
- [Bun](https://bun.sh) v1.0 or later
|
||||
- TypeScript 5.0+
|
||||
- React 19 and react-dom 19 (peer dependencies)
|
||||
|
||||
> **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -152,7 +153,7 @@ bun run dev
|
||||
| Command | Description |
|
||||
| -------------- | ---------------------------------------------------------------------- |
|
||||
| `bunext dev` | Start the development server with HMR and file watching. |
|
||||
| `bunext build` | Bundle all pages for production. Outputs artifacts to `public/pages/`. |
|
||||
| `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
|
||||
@@ -186,7 +187,7 @@ bunext build
|
||||
bunext start
|
||||
```
|
||||
|
||||
> **Note:** `bunext start` will exit with an error if `public/pages/map.json` does not exist. Always run `bunext build` (or `bun run build`) before `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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,9 +209,10 @@ my-app/
|
||||
│ │ └── [slug].tsx # Route: /blog/:slug (dynamic)
|
||||
│ └── api/
|
||||
│ └── users.ts # API route: /api/users
|
||||
├── public/ # Static files and bundler output
|
||||
│ └── __bunext/
|
||||
│ ├── pages/ # Generated by bundler (do not edit manually)
|
||||
├── 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
|
||||
@@ -605,7 +607,7 @@ public/
|
||||
|
||||
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 `public/__bunext/cache/`. Each cached page produces two files:
|
||||
Cache files are stored in `.bunext/public/cache/`. Each cached page produces two files:
|
||||
|
||||
| File | Contents |
|
||||
| ----------------- | ---------------------------------------------- |
|
||||
@@ -670,14 +672,14 @@ Expiry resolution order (first truthy value wins):
|
||||
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 `public/__bunext/`) 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.
|
||||
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 `public/__bunext/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.
|
||||
- **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.
|
||||
|
||||
---
|
||||
@@ -880,7 +882,7 @@ 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. Starts the ESBuild bundler in **watch mode** — it will automatically rebuild when file content changes.
|
||||
4. Starts `Bun.build` in **watch mode** — it will automatically rebuild when file content changes.
|
||||
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()`.
|
||||
@@ -890,26 +892,26 @@ Running `bunext dev`:
|
||||
Running `bunext build`:
|
||||
|
||||
1. Sets `NODE_ENV=production`.
|
||||
2. Runs ESBuild once (not in watch mode) with minification enabled.
|
||||
3. Writes all bundled artifacts to `public/pages/` and the artifact map to `public/pages/map.json`.
|
||||
2. Runs `Bun.build` 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 `public/pages/map.json` to load the pre-built artifact map.
|
||||
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 (`allPagesBundler`) uses ESBuild with three custom plugins:
|
||||
The bundler uses `Bun.build` with the `bun-plugin-tailwind` plugin. For each page, a client hydration entry point is generated and written as a real temporary file under `.bunext/hydration-src/`. Each 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.
|
||||
|
||||
- **`tailwindcss` plugin** — Processes any `.css` files through PostCSS + Tailwind CSS before bundling.
|
||||
- **`virtual-entrypoints` plugin** — Generates an in-memory client hydration entry point for each page. Each 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.
|
||||
- **`artifact-tracker` plugin** — After each build, collects all output file paths, content hashes, and source entrypoints into a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `public/pages/map.json`.
|
||||
React is loaded externally — `react`, `react-dom`, `react-dom/client`, and `react/jsx-runtime` are all marked as external in the `Bun.build` config. The correct React version is resolved from the framework's own `node_modules` at startup and injected into every HTML page via a `<script type="importmap">` pointing at `esm.sh`. This guarantees a single shared React instance across all page bundles and HMR updates regardless of project size.
|
||||
|
||||
Output files are named `[dir]/[name]/[hash]` so filenames change when content changes, enabling cache-busting.
|
||||
After each build, output metadata from `Bun.build`'s metafile is used to map each output file back to its source page, producing a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `.bunext/public/pages/map.json`.
|
||||
|
||||
Output files are named `[hash].[ext]` so filenames change when content changes, enabling cache-busting.
|
||||
|
||||
### Hot Module Replacement
|
||||
|
||||
@@ -949,7 +951,7 @@ Request
|
||||
├── /favicon.* → Serve favicon from public/
|
||||
│
|
||||
└── Everything else → Server-side render a page
|
||||
[Production only] Check public/__bunext/cache/ for key = pathname + search
|
||||
[Production only] Check .bunext/public/cache/ for key = pathname + search
|
||||
Cache HIT → return cached HTML with X-Bunext-Cache: HIT header
|
||||
Cache MISS → continue ↓
|
||||
1. Match route via FileSystemRouter
|
||||
@@ -967,6 +969,7 @@ Request
|
||||
Server-rendered HTML includes:
|
||||
|
||||
- `window.__PAGE_PROPS__` — the serialized server function return value, read by `hydrateRoot` on the client.
|
||||
- A `<script type="importmap">` mapping React package specifiers to the esm.sh CDN (uses the `?dev` build in development).
|
||||
- A `<script type="module" async>` tag pointing to the page's bundled client script.
|
||||
- A `<link rel="stylesheet">` tag if the bundler emitted a CSS file for the page.
|
||||
- In development: the HMR client script.
|
||||
|
||||
Reference in New Issue
Block a user