Bugfixes. Documentation update.
This commit is contained in:
parent
98d3cf7da7
commit
5c53e94a3e
105
README.md
105
README.md
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
10
dist/functions/server/handle-hmr.js
vendored
10
dist/functions/server/handle-hmr.js
vendored
@ -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) {
|
||||
|
||||
@ -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`;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user