From 13bd8bb8519af0dc6c9e230b1b144015cd00863e Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sat, 21 Mar 2026 14:36:32 +0100 Subject: [PATCH] Update README.md --- README.md | 172 +++++++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 106 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 433e33b..19da73e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A server-rendering framework for React, built on [Bun](https://bun.sh). Bunext h 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 @@ -72,11 +73,10 @@ Install Bunext directly from GitHub: bun add github:moduletrace/bunext ``` -Install required peer dependencies: +Install globally: ```bash -bun add react react-dom -bun add -d typescript @types/react @types/react-dom +bun add -g github:moduletrace/bunext ``` --- @@ -102,13 +102,25 @@ export default function HomePage() { } ``` -3. Start the development server: +3. Add scripts to your `package.json`: -```bash -bunext dev +```json +{ + "scripts": { + "dev": "bunx bunext dev", + "build": "bunx bunext build", + "start": "bunx bunext start" + } +} ``` -4. Open `http://localhost:7000` in your browser. +4. Start the development server: + +```bash +bun run dev +``` + +5. Open `http://localhost:7000` in your browser. --- @@ -120,18 +132,38 @@ bunext dev | `bunext build` | Bundle all pages for production. Outputs artifacts to `public/pages/`. | | `bunext start` | Start the production server using pre-built artifacts. | +### Running the CLI + +Bunext exposes a `bunext` binary. How you invoke it depends on how the package is installed: + +**Local install (recommended)** — add scripts to `package.json` and run them with `bun run`: + +```json +{ + "scripts": { + "dev": "bunx bunext dev", + "build": "bunx bunext build", + "start": "bunx bunext start" + } +} +``` + ```bash -# Development +bun run dev +bun run build +bun run start +``` + +**Global install** — install once and use `bunext` from anywhere: + +```bash +bun add -g github:moduletrace/bunext bunext dev - -# Production build bunext build - -# Production server (must run build first) bunext start ``` -> **Note:** `bunext start` will exit with an error if `public/pages/map.json` does not exist. Always run `bunext build` before `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`. --- @@ -184,11 +216,11 @@ Dynamic route parameters (e.g. `[slug]`) are available in the `server` function Directories whose name contains `--` or a parenthesis (`(` or `)`) are completely ignored by the router. Use this to co-locate helper components, utilities, or shared logic directly inside `src/pages/` alongside the routes that use them, without them becoming routes. -| Naming pattern | Effect | -| --- | --- | +| Naming pattern | Effect | +| --------------- | -------------------- | | `(components)/` | Ignored — not routed | -| `--utils--/` | Ignored — not routed | -| `--lib/` | Ignored — not routed | +| `--utils--/` | Ignored — not routed | +| `--lib/` | Ignored — not routed | ``` src/pages/ @@ -222,7 +254,7 @@ Export a `server` function to run server-side logic before rendering. The return ```tsx // src/pages/profile.tsx -import type { BunextPageServerFn } from "bunext/src/types"; +import type { BunextPageServerFn } from "@moduletrace/bunext/types"; type Props = { props?: { username: string; bio: string }; @@ -274,7 +306,7 @@ Every page component automatically receives a `url` prop — a copy of the reque ```tsx // src/pages/index.tsx -import type { BunextPageModuleServerReturnURLObject } from "bunext/src/types"; +import type { BunextPageModuleServerReturnURLObject } from "@moduletrace/bunext/types"; type Props = { url?: BunextPageModuleServerReturnURLObject; @@ -292,20 +324,20 @@ export default function HomePage({ url }: Props) { 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` | `""` | +| 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 @@ -354,7 +386,7 @@ export const server: BunextPageServerFn = async (ctx) => { Export a `meta` object to inject SEO and Open Graph tags into the ``: ```tsx -import type { BunextPageModuleMeta } from "bunext/src/types"; +import type { BunextPageModuleMeta } from "@moduletrace/bunext/types"; export const meta: BunextPageModuleMeta = { title: "My Page Title", @@ -393,7 +425,7 @@ export default function AboutPage() { `meta` can also be an async function that receives the request context and server response: ```tsx -import type { BunextPageModuleMetaFn } from "bunext/src/types"; +import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types"; export const meta: BunextPageModuleMetaFn = async ({ ctx, serverRes }) => { return { @@ -408,7 +440,7 @@ export const meta: BunextPageModuleMetaFn = async ({ ctx, serverRes }) => { Export a `Head` functional component to inject arbitrary HTML into ``. It receives the server response and request context: ```tsx -import type { BunextPageHeadFCProps } from "bunext/src/types"; +import type { BunextPageHeadFCProps } from "@moduletrace/bunext/types"; export function Head({ serverRes, ctx }: BunextPageHeadFCProps) { return ( @@ -454,7 +486,7 @@ Create files under `src/pages/api/` to define API endpoints. The default export ```ts // src/pages/api/hello.ts -import type { BunxRouteParams } from "bunext/src/types"; +import type { BunxRouteParams } from "@moduletrace/bunext/types"; export default async function handler(ctx: BunxRouteParams): Promise { return Response.json({ message: "Hello from the API" }); @@ -465,7 +497,7 @@ API routes are matched at `/api/`. Because the handler returns a plain ```ts // src/pages/api/users.ts -import type { BunxRouteParams } from "bunext/src/types"; +import type { BunxRouteParams } from "@moduletrace/bunext/types"; export default async function handler(ctx: BunxRouteParams): Promise { if (ctx.req.method !== "GET") { @@ -488,7 +520,7 @@ Export a `config` object to override the per-route request body limit (default: import type { BunextServerRouteConfig, BunxRouteParams, -} from "bunext/src/types"; +} from "@moduletrace/bunext/types"; export const config: BunextServerRouteConfig = { maxRequestBodyMB: 50, // allow up to 50 MB @@ -552,10 +584,10 @@ Bunext includes a file-based HTML cache for production. Caching is **disabled in Cache files are stored in `public/__bunext/cache/`. Each cached page produces two files: -| File | Contents | -|---------------------------|----------------------------------------------| -| `.res.html` | The cached HTML response body | -| `.meta.json` | Metadata: creation timestamp, expiry, paradigm | +| File | Contents | +| ----------------- | ---------------------------------------------- | +| `.res.html` | The cached HTML response body | +| `.meta.json` | Metadata: creation timestamp, expiry, paradigm | The cache is **cold on first request**: the first visitor triggers a full server render and writes the cache. Every subsequent request within the expiry window receives the cached HTML directly, bypassing the server function, component render, and bundler lookup. A cache hit is indicated by the response header `X-Bunext-Cache: HIT`. @@ -565,7 +597,7 @@ Export a `config` object from a page file to opt that page into caching: ```tsx // src/pages/products.tsx -import type { BunextRouteConfig } from "bunext/src/types"; +import type { BunextRouteConfig } from "@moduletrace/bunext/types"; export const config: BunextRouteConfig = { cachePage: true, @@ -582,7 +614,7 @@ export default function ProductsPage() { Cache settings can also be returned from the `server` function, which lets you conditionally enable caching based on request data: ```tsx -import type { BunextPageServerFn } from "bunext/src/types"; +import type { BunextPageServerFn } from "@moduletrace/bunext/types"; export const server: BunextPageServerFn = async (ctx) => { const data = await fetchProducts(); @@ -595,7 +627,13 @@ export const server: BunextPageServerFn = async (ctx) => { }; export default function ProductsPage({ props }: any) { - return
    {props.data.map((p: any) =>
  • {p.name}
  • )}
; + return ( +
    + {props.data.map((p: any) => ( +
  • {p.name}
  • + ))} +
+ ); } ``` @@ -627,7 +665,7 @@ Create a `bunext.config.ts` file in your project root to configure Bunext: ```ts // bunext.config.ts -import type { BunextConfig } from "bunext/src/types"; +import type { BunextConfig } from "@moduletrace/bunext/types"; const config: BunextConfig = { port: 3000, // default: 7000 @@ -643,18 +681,18 @@ const config: BunextConfig = { export default config; ``` -| Option | Type | Default | Description | -| -------------- | --------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------- | -| `port` | `number` | `7000` | HTTP server port | -| `origin` | `string` | — | Canonical origin URL | -| `distDir` | `string` | `.bunext` | Internal artifact directory | -| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets | -| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time | -| `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` | — | Bun WebSocket handler — see [WebSocket](#websocket) | -| `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) | +| Option | Type | Default | Description | +| -------------------- | --------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------- | +| `port` | `number` | `7000` | HTTP server port | +| `origin` | `string` | — | Canonical origin URL | +| `distDir` | `string` | `.bunext` | Internal artifact directory | +| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets | +| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time | +| `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` | — | Bun WebSocket handler — see [WebSocket](#websocket) | +| `serverOptions` | `ServeOptions` | — | Extra options passed to `Bun.serve()` (excluding `fetch`) — see [Server Options](#server-options) | ### Middleware @@ -668,7 +706,7 @@ Middleware runs on every request before any routing. Define it in `bunext.config import type { BunextConfig, BunextConfigMiddlewareParams, -} from "bunext/src/types"; +} from "@moduletrace/bunext/types"; const config: BunextConfig = { middleware: async ({ req, url }) => { @@ -721,7 +759,7 @@ export const BunextWebsocket: WebSocketHandler = { ```ts // bunext.config.ts -import type { BunextConfig } from "bunext/src/types"; +import type { BunextConfig } from "@moduletrace/bunext/types"; import { BunextWebsocket } from "./websocket"; const config: BunextConfig = { @@ -737,7 +775,7 @@ Pass additional options to the underlying `Bun.serve()` call via `serverOptions` ```ts // bunext.config.ts -import type { BunextConfig } from "bunext/src/types"; +import type { BunextConfig } from "@moduletrace/bunext/types"; const config: BunextConfig = { serverOptions: { @@ -764,7 +802,7 @@ For full control over the `Bun.serve()` instance — custom WebSocket upgrade lo ```ts // server.ts -import bunext from "bunext"; +import bunext from "@moduletrace/bunext"; const development = process.env.NODE_ENV === "development"; const port = process.env.PORT || 3700; @@ -786,11 +824,11 @@ const server = Bun.serve({ bunext.bunextLog.info(`Server running on http://localhost:${server.port} ...`); ``` -| Export | Type | Description | -|--------|------|-------------| -| `bunextInit()` | `() => Promise` | Initializes config, router, and bundler. Must be called before handling requests. | +| Export | Type | Description | +| ------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `bunextInit()` | `() => Promise` | Initializes config, router, and bundler. Must be called before handling requests. | | `bunextRequestHandler({ req })` | `(params: { req: Request }) => Promise` | 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`). | +| `bunextLog` | Logger | Framework logger (`info`, `error`, `success`, `server`, `watch`). | Run the custom server directly with Bun: diff --git a/package.json b/package.json index 39b9b6f..90e6158 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ ], "scripts": { "dev": "tsc --watch", - "git:push": "tsc --noEmit && tsc && git add . && git commit -m 'Bugfix. Update generate-web-html function' && git push", + "git:push": "tsc --noEmit && tsc && git add . && git commit -m 'Update README.md' && git push", "compile": "bun build ./src/commands/index.ts --compile --outfile bin/bunext", "build": "tsc", "test": "bun test --max-concurrency=1"