Update documentaion.
This commit is contained in:
parent
52dde6c0ab
commit
39f802d26a
73
README.md
73
README.md
@ -1,6 +1,15 @@
|
|||||||
# Bunext
|
# Bunext
|
||||||
|
|
||||||
A Next.js-style full-stack meta-framework built on [Bun](https://bun.sh) and React 19. Bunext provides server-side rendering, file-system based routing, Hot Module Replacement (HMR), and client-side hydration — using ESBuild to bundle client assets and Bun's native HTTP server (`Bun.serve`) to handle requests.
|
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.
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
Bunext is focused on **server-side rendering and processing**. Every page is rendered on the server on every request. The framework deliberately does not implement client-side navigation, SPA routing, or client-side state management — those concerns belong in client-side libraries and are orthogonal to what Bunext is solving.
|
||||||
|
|
||||||
|
The goal is a framework that is:
|
||||||
|
- Fast — Bun's runtime speed and ESBuild'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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -15,6 +24,7 @@ A Next.js-style full-stack meta-framework built on [Bun](https://bun.sh) and Rea
|
|||||||
- [Pages](#pages)
|
- [Pages](#pages)
|
||||||
- [Basic Page](#basic-page)
|
- [Basic Page](#basic-page)
|
||||||
- [Server Function](#server-function)
|
- [Server Function](#server-function)
|
||||||
|
- [Default Server Props](#default-server-props)
|
||||||
- [Redirects from Server](#redirects-from-server)
|
- [Redirects from Server](#redirects-from-server)
|
||||||
- [Custom Response Options](#custom-response-options)
|
- [Custom Response Options](#custom-response-options)
|
||||||
- [SEO Metadata](#seo-metadata)
|
- [SEO Metadata](#seo-metadata)
|
||||||
@ -192,6 +202,7 @@ import type { BunextPageServerFn } from "bunext/src/types";
|
|||||||
type Props = {
|
type Props = {
|
||||||
props?: { username: string; bio: string };
|
props?: { username: string; bio: string };
|
||||||
query?: Record<string, string>;
|
query?: Record<string, string>;
|
||||||
|
url?: URL;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const server: BunextPageServerFn<{
|
export const server: BunextPageServerFn<{
|
||||||
@ -211,11 +222,12 @@ export const server: BunextPageServerFn<{
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProfilePage({ props }: Props) {
|
export default function ProfilePage({ props, url }: Props) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{props?.username}</h1>
|
<h1>{props?.username}</h1>
|
||||||
<p>{props?.bio}</p>
|
<p>{props?.bio}</p>
|
||||||
|
<p>Current path: {url?.pathname}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -231,6 +243,45 @@ The server function receives a `ctx` object (type `BunxRouteParams`) with:
|
|||||||
| `query` | `any` | Query string parameters |
|
| `query` | `any` | Query string parameters |
|
||||||
| `resTransform` | `(res: Response) => Promise<Response>\|Response` | Intercept and transform the final response |
|
| `resTransform` | `(res: Response) => Promise<Response>\|Response` | Intercept and transform the final response |
|
||||||
|
|
||||||
|
### Default Server Props
|
||||||
|
|
||||||
|
Every page component automatically receives a `url` prop — a copy of the request URL object — even if no `server` function is exported. This means you can always read URL data (pathname, search params, origin, etc.) directly from component props without writing a `server` function.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/pages/index.tsx
|
||||||
|
import type { BunextPageModuleServerReturnURLObject } from "bunext/src/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url?: BunextPageModuleServerReturnURLObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HomePage({ url }: Props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Visiting: {url?.pathname}</p>
|
||||||
|
<p>Origin: {url?.origin}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `url` prop exposes the following fields from the standard Web `URL` interface:
|
||||||
|
|
||||||
|
| Field | Type | Example |
|
||||||
|
| -------------- | ---------------- | -------------------------------- |
|
||||||
|
| `href` | `string` | `"https://example.com/blog?q=1"` |
|
||||||
|
| `origin` | `string` | `"https://example.com"` |
|
||||||
|
| `protocol` | `string` | `"https:"` |
|
||||||
|
| `host` | `string` | `"example.com"` |
|
||||||
|
| `hostname` | `string` | `"example.com"` |
|
||||||
|
| `port` | `string` | `""` |
|
||||||
|
| `pathname` | `string` | `"/blog"` |
|
||||||
|
| `search` | `string` | `"?q=1"` |
|
||||||
|
| `searchParams` | `URLSearchParams` | `URLSearchParams { q: "1" }` |
|
||||||
|
| `hash` | `string` | `""` |
|
||||||
|
| `username` | `string` | `""` |
|
||||||
|
| `password` | `string` | `""` |
|
||||||
|
|
||||||
### Redirects from Server
|
### Redirects from Server
|
||||||
|
|
||||||
Return a `redirect` object from the `server` function to redirect the client:
|
Return a `redirect` object from the `server` function to redirect the client:
|
||||||
@ -620,10 +671,7 @@ middleware: async ({ req, url }) => {
|
|||||||
return new Response("Down for maintenance", { status: 503 });
|
return new Response("Down for maintenance", { status: 503 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CORS headers to all API responses
|
// For API routes, return undefined to continue — the handler controls its own Response directly
|
||||||
if (url.pathname.startsWith("/api/")) {
|
|
||||||
// Let the request proceed, but transform the response via resTransform on individual routes
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -679,7 +727,14 @@ Output files are named `[dir]/[name]/[hash]` so filenames change when content ch
|
|||||||
|
|
||||||
### Hot Module Replacement
|
### Hot Module Replacement
|
||||||
|
|
||||||
In development, Bunext injects a script into every HTML page that opens a **Server-Sent Events (SSE)** connection to `/__hmr`. When a rebuild completes and the bundle hash for a page changes, the server pushes an event through the SSE stream and the client reloads the page automatically.
|
In development, Bunext injects a script into every HTML page that opens a **Server-Sent Events (SSE)** connection to `/__hmr`. When a rebuild completes and the bundle hash for a page changes, the server pushes an `update` event through the stream containing the new artifact metadata. The client then performs a **true in-place HMR update** — no full page reload:
|
||||||
|
|
||||||
|
1. If the page has a CSS bundle, the old `<link rel="stylesheet">` is replaced with a new one pointing to the updated CSS file (cache-busted with `?t=<timestamp>`).
|
||||||
|
2. The existing client hydration `<script>` (identified by `id="bunext-client-hydration-script"`) is removed from the DOM.
|
||||||
|
3. A new `<script type="module">` is injected pointing to the freshly rebuilt JS bundle (also cache-busted).
|
||||||
|
4. The new bundle calls `window.__BUNEXT_RERENDER__(NewComponent)` if the root is already mounted, otherwise falls back to a fresh `hydrateRoot`.
|
||||||
|
|
||||||
|
The endpoint `/__bunext_client_hmr__?href=<page-url>` handles on-demand HMR bundle generation: it re-bundles the target page's component on every request so the freshly injected script always reflects the latest source.
|
||||||
|
|
||||||
The SSE controller for each connected client is tracked in `global.HMR_CONTROLLERS`, keyed by the page URL and its bundled artifact map entry. On disconnect, the controller is removed from the list.
|
The SSE controller for each connected client is tracked in `global.HMR_CONTROLLERS`, keyed by the page URL and its bundled artifact map entry. On disconnect, the controller is removed from the list.
|
||||||
|
|
||||||
@ -694,7 +749,9 @@ Request
|
|||||||
│ Returns Response? → short-circuit, send response immediately
|
│ Returns Response? → short-circuit, send response immediately
|
||||||
│ Returns undefined → continue
|
│ Returns undefined → continue
|
||||||
│
|
│
|
||||||
├── GET /__hmr → Open SSE stream for HMR (dev only)
|
├── GET /__hmr → Open SSE stream for HMR (dev only)
|
||||||
|
│
|
||||||
|
├── GET /__bunext_client_hmr__?href= → On-demand HMR bundle for the given page URL (dev only)
|
||||||
│
|
│
|
||||||
├── /api/* → API route handler
|
├── /api/* → API route handler
|
||||||
│ Matches src/pages/api/<path>.ts
|
│ Matches src/pages/api/<path>.ts
|
||||||
|
|||||||
644
comparisons/NEXTJS.md
Normal file
644
comparisons/NEXTJS.md
Normal file
File diff suppressed because it is too large
Load Diff
113
features/FEATURES.md
Normal file
113
features/FEATURES.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Bunext — Planned Features
|
||||||
|
|
||||||
|
Features currently in development or planned for a future release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Middleware Request Mutation
|
||||||
|
|
||||||
|
**Status:** Planned (soon)
|
||||||
|
|
||||||
|
Extend the `middleware` function in `bunext.config.ts` to support returning a `Request` object. This allows the middleware to modify the incoming request — inject headers, attach auth context, set locale — and continue through the normal routing pipeline without short-circuiting.
|
||||||
|
|
||||||
|
The full return contract:
|
||||||
|
|
||||||
|
| Return value | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| `Response` | Short-circuits — response sent immediately, no further routing |
|
||||||
|
| `Request` | Replaces the original request and continues through the pipeline |
|
||||||
|
| `undefined` | Passes through unchanged (current behaviour) |
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bunext.config.ts
|
||||||
|
const config: BunextConfig = {
|
||||||
|
middleware: async ({ req, url, server }) => {
|
||||||
|
// Inject an auth header and continue
|
||||||
|
const token = await verifySession(req);
|
||||||
|
if (token) {
|
||||||
|
const mutated = new Request(req, {
|
||||||
|
headers: {
|
||||||
|
...Object.fromEntries(req.headers),
|
||||||
|
"x-user-id": token.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return mutated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short-circuit if not authenticated on protected routes
|
||||||
|
if (url.pathname.startsWith("/dashboard")) {
|
||||||
|
return Response.redirect("/login", 302);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise continue unchanged
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom Server
|
||||||
|
|
||||||
|
**Status:** In development
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
This enables use cases that require low-level server control:
|
||||||
|
- Custom WebSocket upgrade handling
|
||||||
|
- Custom TLS/SSL configuration
|
||||||
|
- Integrating Bunext into an existing Bun server alongside other handlers
|
||||||
|
- Custom `error` and `lowMemoryMode` options on `Bun.serve()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sass / SCSS Support
|
||||||
|
|
||||||
|
**Status:** Planned
|
||||||
|
|
||||||
|
Add Sass/SCSS support by integrating the `esbuild-sass-plugin` package into the ESBuild pipeline alongside the existing Tailwind plugin. ESBuild does not handle `.scss`/`.sass` files natively — the plugin intercepts those file loads, compiles them via Dart Sass, and returns standard CSS to ESBuild.
|
||||||
|
|
||||||
|
Implementation is straightforward: install `esbuild-sass-plugin`, add it to the plugins array in `allPagesBundler` and `writeHMRTsxModule`. No changes to the rest of the pipeline — CSS extraction, per-page bundling, and HMR CSS swapping all work the same way.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Static Export (`bunext export`)
|
||||||
|
|
||||||
|
**Status:** Planned (low priority)
|
||||||
|
|
||||||
|
Add a `bunext export` command that pre-renders all pages to static HTML files, deployable to a CDN without a running server. This is a convenience feature for projects that have no dynamic server-side requirements.
|
||||||
|
|
||||||
|
A server is a fundamental requirement for Bunext — like WordPress, it is designed to run on a server. Static export is a secondary capability for edge cases, not a primary deployment model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WebSocket Support via Config
|
||||||
|
|
||||||
|
**Status:** Planned
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Proposed config shape:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// bunext.config.ts
|
||||||
|
import type { BunextConfig } from "bunext/src/types";
|
||||||
|
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "@moduletrace/bunext",
|
"name": "@moduletrace/bunext",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"bin": {
|
"bin": {
|
||||||
"bunext": "dist/index.js"
|
"bunext": "dist/index.js"
|
||||||
},
|
},
|
||||||
@ -13,7 +13,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Update HMR. Make it true HMR. Add URL to page server props' && git push",
|
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Update documentaion.' && git push",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user