Bugfixes. Documentation update.

This commit is contained in:
Benjamin Toby 2026-03-21 08:10:02 +01:00
parent 98d3cf7da7
commit 5c53e94a3e
9 changed files with 209 additions and 28 deletions

105
README.md
View File

@ -43,6 +43,9 @@ The goal is a framework that is:
- [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)
@ -650,6 +653,8 @@ export default config;
| `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<any>` | — | Bun WebSocket handler — see [WebSocket](#websocket) |
| `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) |
### Middleware
@ -666,7 +671,7 @@ import type {
} from "bunext/src/types";
const config: BunextConfig = {
middleware: async ({ req, url, server }) => {
middleware: async ({ req, url }) => {
// Example: protect all /dashboard/* routes
if (url.pathname.startsWith("/dashboard")) {
const token = req.headers.get("authorization");
@ -697,6 +702,104 @@ middleware: async ({ req, url }) => {
},
```
### 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<any> = {
message(ws, message) {
console.log(`WS Message => ${message}`);
},
};
```
```ts
// bunext.config.ts
import type { BunextConfig } from "bunext/src/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 "bunext/src/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 "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<void>` | Initializes config, router, and bundler. Must be called before handling requests. |
| `bunextRequestHandler({ req })` | `(params: { req: Request }) => Promise<Response>` | 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

View File

@ -134,6 +134,10 @@ This report compares the two on their overlapping surface — server-side render
| Prefetching | — | ✅ | ✅ |
| `useRouter` / `usePathname` hooks | — | ✅ | ✅ |
| `useSearchParams` hook | — | ✅ | ✅ |
| **Server** | | | |
| WebSocket support | ✅ | ❌ | ❌ |
| Custom server (bring your own `Bun.serve()`) | ✅ | ✅ (custom `server.js`) | ✅ |
| Extra server options (`tls`, `error`, etc.) | ✅ (`serverOptions`) | ✅ | ✅ |
| **Deployment** | | | |
| Self-hosted (any server with runtime) | ✅ | ✅ | ✅ |
| No vendor lock-in | ✅ | ❌ (Vercel-optimised) | ❌ |
@ -358,7 +362,7 @@ The key distinction from SSG: Bunext's cache is **demand-driven**. A site with 1
| `Request` | *(planned)* Replaces the request and continues through the pipeline |
| `undefined` | Passes through unchanged |
The planned `Request` return allows header injection, auth token forwarding, locale detection, or any other request mutation without terminating the pipeline — a clean, standard-API alternative to Next.js's custom `NextResponse.next({ headers: ... })` pattern.
The planned `Request` return allows header injection, auth token forwarding, locale detection, or any other request mutation without terminating the pipeline — a clean, standard-API alternative to Next.js's custom `NextResponse.next({ headers: ... })` pattern. The middleware function receives `{ req, url }`. The server instance is managed internally and does not need to be passed explicitly.
**Next.js** middleware runs per-request in a lightweight Edge Runtime (V8 isolate), with matched route patterns configured via `matcher`. It uses `NextRequest`/`NextResponse` extensions for rewriting, redirecting, and injecting headers without returning a full response.
@ -426,7 +430,7 @@ Next.js provides additional type-safety features:
### Configuration
**Bunext** (`bunext.config.ts`) exposes: `port`, `origin`, `distDir`, `assetsPrefix`, `globalVars`, `development`, `defaultCacheExpiry`, `middleware`.
**Bunext** (`bunext.config.ts`) exposes: `port`, `origin`, `distDir`, `assetsPrefix`, `globalVars`, `development`, `defaultCacheExpiry`, `middleware`, `websocket`, `serverOptions`.
**Next.js** (`next.config.js`) additionally supports:
- `redirects()` — array of redirect rules evaluated at the server level.
@ -592,6 +596,39 @@ Bunext runs on any server where Bun is installed. There is no Vercel platform de
Next.js is technically self-hostable but is architecturally optimised for Vercel — features like ISR, image optimisation, Edge Middleware, and Analytics are either Vercel-only or degraded outside it.
### WebSocket and Custom Server
Bunext ships native WebSocket support via a `websocket` field in `bunext.config.ts`. The value is a Bun `WebSocketHandler` passed directly to `Bun.serve()` — no third-party library, no adapter, no separate process. Upgrade requests are triggered from any API route — only `req` is needed, as the server instance is managed internally by the framework.
Next.js has no built-in WebSocket support. Upgrading a connection requires a custom Node.js server (`server.js`) outside the Next.js framework, which loses access to Next.js's built-in routing and middleware for that connection.
For projects that need full control over `Bun.serve()` — custom TLS, multi-protocol handling, integrating Bunext alongside other handlers — Bunext exports `bunextInit()` and `bunextRequestHandler()` as first-class primitives. The developer owns the server; Bunext handles the request processing:
```ts
// server.ts
import bunext from "bunext";
const development = process.env.NODE_ENV === "development";
await bunext.bunextInit();
const server = Bun.serve({
routes: {
"/*": {
async GET(req) {
return await bunext.bunextRequestHandler({ req });
},
},
},
development,
port: process.env.PORT || 3700,
});
bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`);
```
Next.js requires a `server.js` file that wraps `next()` — a documented but officially discouraged pattern that disables some platform-specific features on Vercel.
### Bun-Native APIs
Server functions and API routes have direct access to Bun's native APIs: `Bun.file()`, `Bun.write()`, the native SQLite driver, `Bun.password`, `Bun.serve` WebSocket support, etc. — without any Node.js compatibility shim or polyfill layer.

View File

@ -8,6 +8,7 @@ export default async function ({ req }) {
? global.BUNDLER_CTX_MAP?.find((m) => m.local_path == match.filePath)
: undefined;
let controller;
let heartbeat;
const stream = new ReadableStream({
start(c) {
controller = c;
@ -16,8 +17,17 @@ export default async function ({ req }) {
page_url: referer_url.href,
target_map,
});
heartbeat = setInterval(() => {
try {
c.enqueue(": keep-alive\n\n");
}
catch {
clearInterval(heartbeat);
}
}, 5000);
},
cancel() {
clearInterval(heartbeat);
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
if (typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0) {

View File

@ -19,6 +19,7 @@ export default async function (params) {
script += `window.addEventListener("error", (e) => __bunext_show_error(e.message, e.filename ? e.filename + ":" + e.lineno + ":" + e.colno : "", e.error?.stack ?? ""));\n`;
script += `window.addEventListener("unhandledrejection", (e) => __bunext_show_error(String(e.reason?.message ?? e.reason), "", e.reason?.stack ?? ""));\n\n`;
script += `const hmr = new EventSource("/__hmr");\n`;
script += `window.BUNEXT_HMR = hmr;\n`;
script += `window.addEventListener("beforeunload", () => hmr.close());\n`;
script += `hmr.addEventListener("update", async (event) => {\n`;
script += ` if (event?.data) {\n`;

View File

@ -19,11 +19,6 @@ const server = Bun.serve({
},
},
},
/**
* Set this to prevent HMR timeout warnings in the
* browser console in development mode.
*/
idleTimeout: development ? 0 : undefined,
development,
port,
});

View File

@ -21,7 +21,7 @@ The full return contract:
```ts
// bunext.config.ts
const config: BunextConfig = {
middleware: async ({ req, url, server }) => {
middleware: async ({ req, url }) => {
// Inject an auth header and continue
const token = await verifySession(req);
if (token) {
@ -49,11 +49,36 @@ const config: BunextConfig = {
## Custom Server
**Status:** In development
**Status:** Shipped
Allow consumer projects to create and fully customize the underlying `Bun.serve()` instance. Instead of Bunext owning the server entirely, the developer can provide their own server setup and integrate Bunext's request handler into it.
Consumer projects can create and fully customize the underlying `Bun.serve()` instance by using Bunext's exported primitives directly. Instead of Bunext owning the server, the developer provides their own `Bun.serve()` call and integrates Bunext's request handler into it.
This enables use cases that require low-level server control:
```ts
import bunext from "bunext";
await bunext.bunextInit();
const server = Bun.serve({
routes: {
"/*": {
async GET(req) {
return await bunext.bunextRequestHandler({ req });
},
},
},
port: 3000,
});
```
Exported primitives:
| Export | Description |
|--------|-------------|
| `bunextInit()` | Initializes config, router, and bundler |
| `bunextRequestHandler({ req })` | Main Bunext request dispatcher |
| `bunextLog` | Framework logger |
Use cases:
- Custom WebSocket upgrade handling
- Custom TLS/SSL configuration
- Integrating Bunext into an existing Bun server alongside other handlers
@ -83,31 +108,31 @@ A server is a fundamental requirement for Bunext — like WordPress, it is desig
## WebSocket Support via Config
**Status:** Planned
**Status:** Shipped
Add a `websocket` parameter to `bunext.config.ts` to handle WebSocket connections without requiring a custom server. This gives most projects a zero-config path to WebSockets while the custom server feature covers advanced use cases.
The `websocket` field in `bunext.config.ts` accepts a Bun [`WebSocketHandler`](https://bun.sh/docs/api/websockets) and passes it directly to `Bun.serve()`. This gives most projects a zero-config path to WebSockets; the custom server feature covers advanced upgrade routing.
Proposed config shape:
```ts
// websocket.ts
import type { WebSocketHandler } from "bun";
export const BunextWebsocket: WebSocketHandler<any> = {
message(ws, message) {
console.log(`WS Message => ${message}`);
},
};
```
```ts
// bunext.config.ts
import type { BunextConfig } from "bunext/src/types";
import { BunextWebsocket } from "./websocket";
const config: BunextConfig = {
websocket: {
message(ws, message) {
ws.send(`echo: ${message}`);
},
open(ws) {
console.log("Client connected");
},
close(ws, code, reason) {
console.log("Client disconnected");
},
},
websocket: BunextWebsocket,
};
export default config;
```
The `websocket` field maps directly to Bun's [`WebSocketHandler`](https://bun.sh/docs/api/websockets) interface, passed through to `Bun.serve()`. WebSocket upgrade requests are handled automatically by the framework before the normal request pipeline runs.
A companion `serverOptions` field is also available to pass any other `Bun.serve()` options (TLS, error handler, `maxRequestBodySize`, etc.) without needing a custom server.

View File

@ -14,7 +14,7 @@
],
"scripts": {
"dev": "tsc --watch",
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Update Version' && git push",
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Bugfixes. Documentation update.' && git push",
"compile": "bun build ./src/commands/index.ts --compile --outfile bin/bunext",
"build": "tsc"
},

View File

@ -17,6 +17,7 @@ export default async function ({ req }: Params): Promise<Response> {
: undefined;
let controller: ReadableStreamDefaultController<string>;
let heartbeat: ReturnType<typeof setInterval>;
const stream = new ReadableStream<string>({
start(c) {
controller = c;
@ -25,8 +26,16 @@ export default async function ({ req }: Params): Promise<Response> {
page_url: referer_url.href,
target_map,
});
heartbeat = setInterval(() => {
try {
c.enqueue(": keep-alive\n\n");
} catch {
clearInterval(heartbeat);
}
}, 5000);
},
cancel() {
clearInterval(heartbeat);
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);

View File

@ -28,6 +28,7 @@ export default async function (params?: Params) {
script += `window.addEventListener("unhandledrejection", (e) => __bunext_show_error(String(e.reason?.message ?? e.reason), "", e.reason?.stack ?? ""));\n\n`;
script += `const hmr = new EventSource("/__hmr");\n`;
script += `window.BUNEXT_HMR = hmr;\n`;
script += `window.addEventListener("beforeunload", () => hmr.close());\n`;
script += `hmr.addEventListener("update", async (event) => {\n`;
script += ` if (event?.data) {\n`;