@bractjs/bractjs 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +773 -465
  2. package/bin/cli.ts +23 -3
  3. package/package.json +1 -1
  4. package/src/__tests__/build-path.test.ts +29 -0
  5. package/src/__tests__/codegen.test.ts +36 -0
  6. package/src/__tests__/compile-safety.test.ts +163 -0
  7. package/src/__tests__/compile-smoke.test.ts +276 -0
  8. package/src/__tests__/csp.test.ts +80 -0
  9. package/src/__tests__/fixtures/app/routes/_index.tsx +6 -2
  10. package/src/__tests__/fixtures/app/routes/protected.tsx +15 -0
  11. package/src/__tests__/fixtures/app/routes/redirect-action.tsx +13 -0
  12. package/src/__tests__/integration.test.ts +62 -0
  13. package/src/__tests__/layout-registry.test.ts +23 -0
  14. package/src/__tests__/loader.test.ts +23 -0
  15. package/src/__tests__/middleware.test.ts +22 -0
  16. package/src/__tests__/programmatic-api.test.ts +41 -2
  17. package/src/__tests__/response.test.ts +54 -1
  18. package/src/__tests__/security.test.ts +35 -0
  19. package/src/__tests__/server-module-stub.test.ts +145 -0
  20. package/src/__tests__/stream-handler.test.ts +36 -0
  21. package/src/__tests__/typed-routing.test.ts +189 -0
  22. package/src/build/bundler.ts +46 -20
  23. package/src/build/directives.ts +2 -2
  24. package/src/build/env-plugin.ts +63 -0
  25. package/src/build/react-dedupe.ts +41 -0
  26. package/src/client/ClientRouter.tsx +22 -8
  27. package/src/client/build-path.ts +24 -0
  28. package/src/client/components/Form.tsx +10 -1
  29. package/src/client/components/Link.tsx +31 -8
  30. package/src/client/hooks/useFetcher.ts +17 -1
  31. package/src/client/hooks/useNavigate.ts +46 -0
  32. package/src/client/hooks/useParams.ts +15 -4
  33. package/src/client/hooks/useSearchParams.ts +16 -6
  34. package/src/client/nav-utils.ts +54 -3
  35. package/src/client/registry.ts +107 -0
  36. package/src/client/types.ts +3 -0
  37. package/src/codegen/route-codegen.ts +62 -23
  38. package/src/config/load.ts +50 -2
  39. package/src/dev/devtools.ts +72 -39
  40. package/src/dev/hmr-module-handler.ts +6 -4
  41. package/src/dev/rebuilder.ts +16 -1
  42. package/src/dev/server.ts +3 -0
  43. package/src/index.ts +30 -3
  44. package/src/server/csp.ts +92 -0
  45. package/src/server/csrf.ts +44 -6
  46. package/src/server/layout.ts +12 -2
  47. package/src/server/loader.ts +5 -7
  48. package/src/server/render.ts +29 -10
  49. package/src/server/request-handler.ts +15 -4
  50. package/src/server/response.ts +58 -5
  51. package/src/server/serve.ts +10 -0
  52. package/src/server/static.ts +11 -1
  53. package/src/server/stream-handler.ts +8 -7
  54. package/src/server/use-client-runtime.ts +62 -0
  55. package/src/shared/meta-tags.tsx +46 -0
  56. package/types/index.d.ts +67 -5
package/README.md CHANGED
@@ -1,27 +1,152 @@
1
1
  # BractJS
2
2
 
3
- > Production-grade SSR framework for Bun + React.
4
- > File-based routing · Parallel loaders · Streaming SSR · Built-in HMR · Server Actions
3
+ [![npm version](https://img.shields.io/npm/v/@bractjs/bractjs)](https://www.npmjs.com/package/@bractjs/bractjs)
4
+ [![license](https://img.shields.io/npm/l/@bractjs/bractjs)](LICENSE)
5
+
6
+ > Production-grade SSR framework for **Bun + React 19**.
7
+ > File-based routing · Parallel loaders · Streaming SSR · Built-in HMR · Server Actions · Typed routes · Single-binary deploy.
8
+
9
+ ## Requirements
10
+
11
+ - [Bun](https://bun.sh) ≥ 1.1 — no Node.js support
12
+ - React 19 (peer dependency)
13
+
14
+ This README is a **step-by-step guide to every function and feature** BractJS exports. Each section is self-contained and ordered from "first app" to "advanced". Every symbol shown here is a real export from `@bractjs/bractjs` (see [src/index.ts](src/index.ts)).
15
+
16
+ ---
17
+
18
+ ## Table of Contents
19
+
20
+ 1. [Install & create an app](#1-install--create-an-app)
21
+ 2. [Project structure](#2-project-structure)
22
+ 3. [The root layout (`app/root.tsx`)](#3-the-root-layout-approotsx)
23
+ 4. [File-based routing](#4-file-based-routing)
24
+ 5. [Route module API: `loader`, `action`, `meta`, `beforeLoad`, `ErrorBoundary`, `default`](#5-route-module-api)
25
+ 6. [Response helpers: `json`, `redirect`, `error`, `HttpError`](#6-response-helpers)
26
+ 7. [Per-route context: `defineContext`](#7-per-route-context-definecontext)
27
+ 8. [Streaming data: `defer`, `Deferred`, `isDeferred`, `<Await>`](#8-streaming-data)
28
+ 9. [Client hooks](#9-client-hooks)
29
+ 10. [Client components: `<Outlet>`, `<Link>`, `<Form>`, `<Scripts>`, `<LiveReload>`, `<Image>`](#10-client-components)
30
+ 11. [Server Actions (`"use server"`) & client-only (`"use client"`)](#11-server-actions--client-components)
31
+ 12. [Typed API routes: `route` + `createClient`](#12-typed-api-routes)
32
+ 13. [Input validation: `validate`](#13-input-validation-validate)
33
+ 14. [Middleware: `pipeline`, `requestLogger`, `cors`, `authGuard`, `csp`](#14-middleware)
34
+ 15. [Sessions: `createCookieSession`](#15-sessions)
35
+ 16. [Lifecycle hooks: `defineLifecycle`](#16-lifecycle-hooks)
36
+ 17. [Environment variables & `*.server.ts`](#17-environment-variables)
37
+ 18. [Typed routes](#18-typed-routes)
38
+ 19. [Internationalization (i18n) utilities](#19-internationalization-utilities)
39
+ 20. [Image optimization (`<Image>` + `/_image`)](#20-image-optimization)
40
+ 21. [Build & run: CLI + programmatic API (`createDevServer`, `runBuild`, `loadUserConfig`)](#21-build--run)
41
+ 22. [Single-binary deployment (`bun build --compile`)](#22-single-binary-deployment)
42
+ 23. [Custom adapters (`BunAdapter`, Cloudflare)](#23-custom-adapters)
43
+ 24. [Build plugins (for custom `Bun.build`)](#24-build-plugins)
44
+ 25. [Configuration reference](#25-configuration-reference)
45
+ 26. [Full export index](#26-full-export-index)
5
46
 
6
47
  ---
7
48
 
8
- ## Quick Start
49
+ ## 1. Install & create an app
50
+
51
+ BractJS requires [Bun](https://bun.sh). There is no Node.js runtime path.
9
52
 
10
53
  ```sh
11
- # Requires Bun https://bun.sh
54
+ # Scaffold a new app
12
55
  bunx bractjs new my-app
13
56
  cd my-app
57
+
58
+ # Start the dev server (HMR on http://localhost:3000)
14
59
  bun run dev
15
- # → http://localhost:3000
16
60
  ```
17
61
 
18
- > **From source (pre-publish):** clone the repo, then `bun run bin/cli.ts new my-app`.
62
+ `bractjs new <name>` copies the scaffold template, runs `bun install`, and seeds `app/_generated/` so the single-binary entry typechecks before your first build.
63
+
64
+ Add it to an existing project instead:
65
+
66
+ ```sh
67
+ bun add @bractjs/bractjs react react-dom
68
+ ```
69
+
70
+ `react` and `react-dom` v19 are **peer dependencies** — BractJS ships zero other runtime deps.
71
+
72
+ ---
73
+
74
+ ## 2. Project structure
75
+
76
+ ```
77
+ my-app/
78
+ ├── app/
79
+ │ ├── root.tsx # required — the <html> document shell
80
+ │ ├── server.ts # single-binary entry (bun build --compile)
81
+ │ ├── lifecycle.ts # optional — onStart / onShutdown / onError
82
+ │ ├── route-types.gen.ts # generated by `bractjs codegen`
83
+ │ ├── _generated/ # generated by `bractjs codegen:registry` / `:manifest`
84
+ │ ├── actions.ts # "use server" actions (optional)
85
+ │ └── routes/
86
+ │ ├── _index.tsx # → /
87
+ │ ├── about.tsx # → /about
88
+ │ ├── blog/
89
+ │ │ ├── layout.tsx # wraps /blog/*
90
+ │ │ ├── _index.tsx # → /blog
91
+ │ │ └── [id].tsx # → /blog/:id
92
+ │ └── docs/
93
+ │ └── [...slug].tsx # → /docs/* (catch-all)
94
+ ├── public/ # static assets, served at /public/*
95
+ ├── bractjs.config.ts # optional config (see §25)
96
+ └── build/ # generated — do not edit
97
+ ```
98
+
99
+ Defaults: `appDir="./app"`, `publicDir="./public"`, `buildDir="./build"`, `port=3000`. All overridable (§25).
100
+
101
+ ---
102
+
103
+ ## 3. The root layout (`app/root.tsx`)
104
+
105
+ **Required.** It provides the `<html>` document shell and is always rendered. Use the `<Outlet>`, `<Scripts>`, and `<LiveReload>` components from the package.
106
+
107
+ ```tsx
108
+ // app/root.tsx
109
+ import { Outlet, Scripts, LiveReload } from "@bractjs/bractjs";
110
+
111
+ export function meta() {
112
+ return [
113
+ { title: "My App" },
114
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
115
+ ];
116
+ }
117
+
118
+ export default function Root() {
119
+ return (
120
+ <html lang="en">
121
+ <head>
122
+ <meta charSet="utf-8" />
123
+ {/* BractJS hoists <title>/<meta> from every route's meta() into <head> */}
124
+ </head>
125
+ <body>
126
+ <Outlet /> {/* renders the matched route tree */}
127
+ <Scripts /> {/* injects the client bundle + bootstrap data */}
128
+ <LiveReload /> {/* dev-only HMR client; no-op in production */}
129
+ </body>
130
+ </html>
131
+ );
132
+ }
133
+ ```
134
+
135
+ **Step by step:**
136
+
137
+ 1. `export default function Root()` returns the full `<html>` document.
138
+ 2. Put `<Outlet />` where the page content should render.
139
+ 3. Put `<Scripts />` at the end of `<body>` — it's a marker the SSR pipeline replaces with the hashed client entry + `window.__BRACTJS_DATA__`.
140
+ 4. `<LiveReload />` renders an HMR client script in dev and `null` in production.
141
+ 5. Optionally `export function meta()` for site-wide defaults (route `meta()` overrides per descriptor).
142
+
143
+ > `<title>`/`<meta>` tags from any route's `meta()` are rendered into `<head>` via React 19 document-metadata hoisting — you do not place them manually.
19
144
 
20
145
  ---
21
146
 
22
- ## File-Based Routing
147
+ ## 4. File-based routing
23
148
 
24
- Place files inside `app/routes/`. BractJS scans them at startup.
149
+ Drop a file in `app/routes/`; it becomes a route. BractJS scans at startup and builds a trie.
25
150
 
26
151
  | File | URL |
27
152
  |------|-----|
@@ -32,95 +157,162 @@ Place files inside `app/routes/`. BractJS scans them at startup.
32
157
  | `routes/docs/[...slug].tsx` | `/docs/*` (catch-all) |
33
158
  | `routes/blog/layout.tsx` | wraps all `/blog/*` routes |
34
159
 
35
- `app/root.tsx` is the outermost layout (always rendered). Match priority per segment: **static > dynamic > catch-all**.
160
+ - `[param]` a dynamic segment, read via `useParams()` / `params` arg.
161
+ - `[...name]` → a catch-all; the rest of the path lands in `params.name`.
162
+ - `layout.tsx` in any directory wraps every route under it (layouts nest: `root → blog/layout → blog/[id]`).
163
+ - Match priority per segment: **static > dynamic > catch-all**.
164
+
165
+ No registration step — the file IS the route.
36
166
 
37
167
  ---
38
168
 
39
- ## Route Module API
169
+ ## 5. Route module API
40
170
 
41
- Every file in `app/routes/` can export any combination of these:
171
+ Every file in `app/routes/` (and `root.tsx`/`layout.tsx`) may export any combination of these. Import the arg types from the package.
42
172
 
43
173
  ```tsx
44
174
  import type { LoaderArgs, ActionArgs, MetaArgs } from "@bractjs/bractjs";
45
- import { redirect } from "@bractjs/bractjs";
175
+ import { redirect, json, HttpError } from "@bractjs/bractjs";
46
176
 
47
- // Runs on every GET return value becomes useLoaderData()
177
+ // 1) loader — runs on every GET. Return value useLoaderData().
48
178
  export async function loader({ request, params, context }: LoaderArgs) {
49
179
  const post = await db.post.findById(params.id);
50
- if (!post) throw new Response("Not Found", { status: 404 });
180
+ if (!post) throw new HttpError(404, "Not found"); // 404 page
51
181
  return { post };
52
182
  }
53
-
54
183
  export type LoaderData = Awaited<ReturnType<typeof loader>>;
55
184
 
56
- // Runs on POST / PUT / DELETE
57
- export async function action({ params, formData }: ActionArgs) {
185
+ // 2) action — runs on POST / PUT / DELETE / PATCH.
186
+ export async function action({ request, params, context, formData }: ActionArgs) {
58
187
  await db.post.update(params.id, { title: formData.get("title") as string });
59
188
  return redirect("/blog");
60
189
  }
61
190
 
62
- // SSR <title> and <meta> tags
63
- export function meta({ loaderData }: MetaArgs<LoaderData>) {
191
+ // 3) meta — SSR <title> / <meta>. Receives this route's loaderData slice.
192
+ export function meta({ loaderData, params }: MetaArgs<LoaderData>) {
64
193
  return [
65
194
  { title: loaderData.post.title },
66
195
  { name: "description", content: loaderData.post.excerpt },
196
+ { property: "og:title", content: loaderData.post.title },
67
197
  ];
68
198
  }
69
199
 
70
- // Error boundary for this route segment
71
- export function ErrorBoundary({ error }: { error: Error }) {
72
- return <p>Error: {error.message}</p>;
200
+ // 4) beforeLoad auth/redirect gate. Runs BEFORE loaders, on full-page GET
201
+ // AND the /_data soft-nav endpoint. Return a Response to short-circuit.
202
+ export function beforeLoad({ context, params, location }) {
203
+ if (!context.user) {
204
+ return redirect(`/login?next=${encodeURIComponent(location.pathname)}`);
205
+ }
206
+ }
207
+
208
+ // 5) ErrorBoundary — renders when this segment's loader/component throws.
209
+ export function ErrorBoundary({ error }: { error: unknown }) {
210
+ return <p>Something broke: {error instanceof Error ? error.message : String(error)}</p>;
73
211
  }
74
212
 
75
- // The page component (required)
213
+ // 6) default — the page component (required for a renderable route).
76
214
  export default function BlogPost() {
77
215
  const { post } = useLoaderData<LoaderData>();
78
216
  return <article><h1>{post.title}</h1></article>;
79
217
  }
80
218
  ```
81
219
 
220
+ **Execution order for a request:**
221
+
222
+ ```
223
+ beforeLoad → (action, if mutating method) → loaders (root + layouts + route, in parallel) → render
224
+ ```
225
+
226
+ - **Loaders run concurrently** (root, every layout, and the route loader all in one `Promise.all`).
227
+ - A loader that throws an `HttpError`/redirect `Response` is intentional control flow. Any *other* thrown error is caught, sanitized (generic message in production, full message+stack only when `NODE_ENV=development`), and rendered via the nearest `ErrorBoundary`.
228
+
229
+ > **Security:** put auth checks in `beforeLoad` (per route) or middleware (cross-cutting) — never in a component. `/_data` (used by `<Link>` soft-nav) runs `beforeLoad` and the loader, so a component-only check would still leak loader JSON. See §14.
230
+
82
231
  ---
83
232
 
84
- ## Root Layout (`app/root.tsx`)
233
+ ## 6. Response helpers
85
234
 
86
- Required. Provides the `<html>` document shell.
235
+ Imported from `@bractjs/bractjs`.
87
236
 
88
- ```tsx
89
- import { Scripts, LiveReload, Outlet } from "@bractjs/bractjs";
237
+ ```ts
238
+ import { json, redirect, error, HttpError, isRedirect, isHttpError } from "@bractjs/bractjs";
239
+ ```
90
240
 
91
- export function meta() {
92
- return [{ title: "My App" }, { name: "viewport", content: "width=device-width, initial-scale=1" }];
241
+ ### `json(data, init?)`
242
+ Serialize a value as `application/json`.
243
+ ```ts
244
+ return json({ ok: true }, { status: 201 });
245
+ ```
246
+
247
+ ### `redirect(url, status?, headers?, options?)`
248
+ Throw or return a redirect. **Open-redirect safe by default** — rejects `//evil.com`, `/\evil`, `https://…`, `javascript:` unless you pass `{ allowExternal: true }`.
249
+ ```ts
250
+ return redirect("/dashboard"); // 302
251
+ return redirect("/login", 303); // custom status
252
+ return redirect("/x", 302, { "Set-Cookie": cookie }); // with headers
253
+ return redirect("https://other.com", 302, undefined, { allowExternal: true });
254
+ ```
255
+
256
+ ### `error(message, status?)`
257
+ Convenience JSON error: `{ "error": message }` with the given status (default 500).
258
+ ```ts
259
+ return error("Bad Request", 400);
260
+ ```
261
+
262
+ ### `HttpError` & `BractJSError`
263
+ Throw a typed HTTP error from a loader/action. The framework converts it to a response with that status (and a default status text if you omit the message).
264
+ ```ts
265
+ throw new HttpError(403); // → 403 Forbidden
266
+ throw new HttpError(404, "No such post"); // → 404 with custom message
267
+ ```
268
+ `isRedirect(value)` / `isHttpError(value)` / `isBractJSError(value)` are type guards if you handle errors manually.
269
+
270
+ ---
271
+
272
+ ## 7. Per-route context: `defineContext`
273
+
274
+ A route can compute request-scoped data once and share it with all its loaders/actions via the `context` argument. Middleware can also populate `context` (§14) — `defineContext` is the per-route version.
275
+
276
+ ```ts
277
+ // app/routes/dashboard.tsx
278
+ import { defineContext } from "@bractjs/bractjs";
279
+ import { getUser } from "../auth.server.ts";
280
+
281
+ export const context = defineContext(async ({ request, params }) => ({
282
+ user: await getUser(request),
283
+ }));
284
+
285
+ export function beforeLoad({ context }) {
286
+ if (!context.user) return redirect("/login");
93
287
  }
94
288
 
95
- export default function Root() {
96
- return (
97
- <html lang="en">
98
- <head>{/* BractJS injects <title> and <meta> tags here */}</head>
99
- <body>
100
- <Outlet /> {/* current route tree */}
101
- <Scripts /> {/* client bundle */}
102
- <LiveReload /> {/* dev-only HMR — no-op in production */}
103
- </body>
104
- </html>
105
- );
289
+ export async function loader({ context }) {
290
+ return { name: context.user.name };
106
291
  }
107
292
  ```
108
293
 
294
+ The factory runs before `beforeLoad` and its result is merged into `context` for `beforeLoad`, `loader`, and `action` on that route.
295
+
109
296
  ---
110
297
 
111
- ## Deferred / Streaming Data
298
+ ## 8. Streaming data
112
299
 
113
- `defer()` streams slow data without blocking the initial HTML response.
300
+ Stream slow data without blocking the initial HTML.
114
301
 
115
- ```tsx
116
- import { defer } from "@bractjs/bractjs";
302
+ ```ts
303
+ import { defer, Deferred, isDeferred } from "@bractjs/bractjs";
117
304
  import { Await } from "@bractjs/bractjs";
118
305
  import { Suspense } from "react";
306
+ ```
307
+
308
+ ### `defer(data)`
309
+ Wraps each `Promise` field in a `Deferred`; non-promise fields pass through. Awaited fields are in the initial HTML; promises stream after.
119
310
 
311
+ ```tsx
120
312
  export async function loader({ params }: LoaderArgs) {
121
313
  return defer({
122
- post: await db.post.findById(params.id), // awaited in initial HTML
123
- comments: db.comments.forPost(params.id), // Promise streamed later
314
+ post: await db.post.findById(params.id), // awaited initial HTML
315
+ comments: db.comments.forPost(params.id), // Promise streamed
124
316
  });
125
317
  }
126
318
 
@@ -139,176 +331,135 @@ export default function BlogPost() {
139
331
  }
140
332
  ```
141
333
 
142
- ---
143
-
144
- ## Client Primitives
334
+ ### `<Await resolve={promise} fallback={…}>{(data) => …}</Await>`
335
+ Unwraps a promise with React 19's `use()` inside its own `<Suspense>`. `isDeferred(value)` and the `Deferred` class are exported if you need to detect/construct deferred values manually.
145
336
 
146
- ### `<Link>`
337
+ ---
147
338
 
148
- Soft-navigates without a full reload. `prefetch="hover"` preloads the route chunk + loader data on mouse-enter.
339
+ ## 9. Client hooks
149
340
 
150
- ```tsx
151
- import { Link } from "@bractjs/bractjs";
341
+ All hooks are SSR-safe (they return sensible values during SSR) and imported from `@bractjs/bractjs`.
152
342
 
153
- <Link to="/blog/42">Read Post</Link>
154
- <Link to="/about" prefetch="hover">About</Link>
343
+ ### `useLoaderData<T>()` → `T`
344
+ The current route's loader return value.
345
+ ```ts
346
+ const { post } = useLoaderData<LoaderData>();
155
347
  ```
156
348
 
157
- ### `<Form>`
158
-
159
- Fetch-based submission. Re-runs the current route's loader after the action completes.
160
-
161
- ```tsx
162
- import { Form } from "@bractjs/bractjs";
163
-
164
- <Form method="post" action="/blog/new">
165
- <input name="title" />
166
- <button type="submit">Create</button>
167
- </Form>
349
+ ### `useActionData<T>()` → `T | null`
350
+ The most recent action return value (null until an action runs).
351
+ ```ts
352
+ const result = useActionData<{ error?: string }>();
168
353
  ```
169
354
 
170
- ### `<Outlet>`
171
-
172
- Renders the matched child route inside a layout.
173
-
174
- ```tsx
175
- export default function BlogLayout() {
176
- return (
177
- <div>
178
- <nav>Blog</nav>
179
- <Outlet />
180
- </div>
181
- );
182
- }
355
+ ### `useParams<T>()` → `T`
356
+ URL dynamic params. Pass the **route pattern** as a generic to type the result against your codegen'd routes (see §18); an object shape also works.
357
+ ```ts
358
+ const { id } = useParams<"/blog/:id">(); // { id: string } — typed from routes
359
+ const { id } = useParams<{ id: string }>(); // or a hand-written shape
183
360
  ```
361
+ > The pattern is supplied by the caller because the framework can't infer the active route at the type level (React Router's `useParams` works the same way).
184
362
 
185
- ---
186
-
187
- ## Hooks
188
-
189
- | Hook | Returns | Description |
190
- |------|---------|-------------|
191
- | `useLoaderData<T>()` | `T` | Loader return value for the current route |
192
- | `useActionData<T>()` | `T \| null` | Most recent action return value |
193
- | `useParams<T>()` | `T` | URL dynamic params (generic for typed params) |
194
- | `useNavigation()` | `{ state }` | `"idle"` \| `"loading"` \| `"submitting"` |
195
- | `useFetcher()` | `{ data, state, load, submit }` | Background fetch without navigation |
196
-
197
- ```tsx
198
- import { useLoaderData, useNavigation, useFetcher } from "@bractjs/bractjs";
199
-
200
- const { post } = useLoaderData<LoaderData>();
201
-
363
+ ### `useNavigation()` → `{ state }`
364
+ `"idle" | "loading" | "submitting"`.
365
+ ```ts
202
366
  const { state } = useNavigation();
203
367
  if (state === "loading") return <Spinner />;
204
-
205
- const fetcher = useFetcher();
206
- fetcher.load("/api/suggestions?q=bun");
207
368
  ```
208
369
 
209
- ---
210
-
211
- ## Image Optimization
212
-
213
- `<Image>` serves responsively-sized, format-converted images through a built-in `/_image` endpoint. Requires [ImageMagick](https://imagemagick.org) (`magick` or `convert`) — falls back to serving the original if not installed.
214
-
215
- ```tsx
216
- import { Image } from "@bractjs/bractjs";
217
-
218
- // Basic — lazy, WebP, 80% quality, responsive srcset
219
- <Image src="/public/hero.jpg" alt="Hero" width={1200} height={600} />
220
-
221
- // Above-the-fold — eager load + fetchpriority=high
222
- <Image src="/public/hero.jpg" alt="Hero" width={1200} priority />
223
-
224
- // Custom format / quality / fit
225
- <Image
226
- src="/public/photo.jpg"
227
- alt="Photo"
228
- width={800}
229
- format="avif"
230
- quality={70}
231
- fit="contain"
232
- sizes="(max-width: 640px) 100vw, 50vw"
233
- />
370
+ ### `useNavigate()` → `(to, { params? }) => Promise<void>`
371
+ Imperative soft navigation — the counterpart to `<Link>`. `to` autocompletes your routes (after codegen, §18) and `params` is typed per route; any string is still accepted.
372
+ ```ts
373
+ const navigate = useNavigate();
374
+ await navigate("/blog/:id", { params: { id: "42" } }); // typed
375
+ await navigate("/about"); // static
376
+ await navigate(`/blog/${id}`); // built string (also fine)
234
377
  ```
235
378
 
236
- The component generates a `srcset` across up to 7 breakpoints (320 1920 px). Optimized images are cached in memory (LRU, 200 slots) and on disk (`.bract-image-cache/`, survives restarts). Both layers respond with `Cache-Control: immutable`.
237
-
238
- **Props:**
239
-
240
- | Prop | Type | Default | Description |
241
- |------|------|---------|-------------|
242
- | `src` | `string` | | Path under `/public/` |
243
- | `alt` | `string` | — | Alt text (required) |
244
- | `width` | `number` | — | Intrinsic width |
245
- | `height` | `number` | — | Intrinsic height |
246
- | `quality` | `number` | `80` | 1–100 |
247
- | `format` | `"webp" \| "avif" \| "jpeg" \| "png"` | `"webp"` | Output format |
248
- | `fit` | `"cover" \| "contain" \| "fill"` | `"cover"` | Resize mode |
249
- | `priority` | `boolean` | `false` | Disable lazy loading, set `fetchpriority=high` |
250
- | `sizes` | `string` | `"100vw"` | HTML `sizes` attribute |
251
-
252
- ---
253
-
254
- ## Typed Routes (Codegen)
255
-
256
- Run `bractjs codegen` to generate `app/route-types.gen.ts` from your route files. This runs automatically during `bractjs build`.
257
-
258
- ```sh
259
- bractjs codegen # reads ./app, writes ./app/route-types.gen.ts
260
- bractjs codegen ./app ./app/route-types.gen.ts # explicit paths
379
+ ### `useSearchParams<T>()` → `{ searchParams, getParam, setSearchParams }`
380
+ Read/write URL query params; writing triggers a soft-nav loader re-run. Pass the route pattern as a generic to type the result against `RouteSearchParamsMap` (augment it per route, §18); an object shape also works.
381
+ ```ts
382
+ const { searchParams, getParam, setSearchParams } = useSearchParams<"/blog/:id">();
383
+ const q = getParam("q"); // string | null
384
+ setSearchParams({ q: "bun" }); // replace all params
385
+ setSearchParams((prev) => { prev.set("page", "2"); return prev; }); // update
261
386
  ```
262
387
 
263
- **Generated file provides:**
264
-
388
+ ### `useFetcher()` → `{ data, state, load, submit }`
389
+ Background fetch without navigating.
265
390
  ```ts
266
- // Every URL pattern as a string literal union
267
- export type AppRoutes = "/" | "/blog/:id" | "/org/:orgId/repo/:repoId";
391
+ const fetcher = useFetcher();
392
+ await fetcher.load("/products?q=bun"); // GET loader data
393
+ await fetcher.submit("/cart", { method: "post", body: { id: "1" } });
394
+ ```
268
395
 
269
- // Per-route typed params
270
- export type RouteParams<T extends AppRoutes> =
271
- T extends "/blog/:id" ? { id: string } :
272
- T extends "/org/:orgId/repo/:repoId" ? { orgId: string; repoId: string } :
273
- Record<never, never>;
396
+ ### `useFetcher<T>({ stream: true })` → `{ connect }`
397
+ Consume an async-generator server action as an SSE stream.
398
+ ```ts
399
+ const { connect } = useFetcher<string>({ stream: true });
400
+ for await (const chunk of connect(actionId)) { /* … */ }
401
+ ```
274
402
 
275
- // Typed loader / action args
276
- export type TypedLoaderArgs<T extends AppRoutes> = { request: Request; params: RouteParams<T>; context: Record<string, unknown> };
277
- export type TypedActionArgs<T extends AppRoutes> = TypedLoaderArgs<T> & { formData: FormData };
403
+ ### `useBlocker(shouldBlock)`
404
+ Prompt before leaving when there are unsaved changes (intercepts back/forward and `<Link>` navigations).
405
+ ```ts
406
+ useBlocker(() => formIsDirty);
407
+ ```
278
408
 
279
- // Type-safe route builder
280
- export const routes = {
281
- "/": () => "/",
282
- "/blog/:id": (params: { id: string }) => `/blog/${params.id}`,
283
- } as const;
409
+ ### `useLocale(defaultLocale?)` `string` and `useLocalizedLink(defaultLocale?)` → `(path) => string`
410
+ For i18n prefix routing (§19).
411
+ ```ts
412
+ const locale = useLocale("en"); // reads params.locale
413
+ const localized = useLocalizedLink("en");
414
+ <Link to={localized("/about")} /> // → /en/about
284
415
  ```
285
416
 
286
- **Usage:**
417
+ ---
287
418
 
288
- ```ts
289
- // Typed loader — params.id is string, not string | undefined
290
- import type { TypedLoaderArgs, RouteParams } from "../route-types.gen.ts";
419
+ ## 10. Client components
291
420
 
292
- export async function loader({ params }: TypedLoaderArgs<"/blog/:id">) {
293
- return db.post.findById(params.id); // typed
421
+ ### `<Outlet />`
422
+ Renders the matched child route inside a layout (or the route tree inside `root.tsx`).
423
+ ```tsx
424
+ export default function BlogLayout() {
425
+ return <div><nav>Blog</nav><Outlet /></div>;
294
426
  }
427
+ ```
295
428
 
296
- // Typed params hook
297
- const { id } = useParams<RouteParams<"/blog/:id">>();
429
+ ### `<Link to params? prefetch? viewTransition?>`
430
+ Soft-navigates without a full reload. After codegen (§18), `to` autocompletes your routes; for a dynamic route pass typed `params`. Building the URL yourself still works, so existing links need no changes.
431
+ ```tsx
432
+ <Link to="/blog/:id" params={{ id: "42" }}>Read</Link> {/* typed route + params */}
433
+ <Link to={`/blog/${id}`}>Read</Link> {/* built string — also fine */}
434
+ <Link to="/about" prefetch="hover">About</Link> {/* preload chunk + loader on hover */}
435
+ <Link to="/gallery" viewTransition>Gallery</Link> {/* use View Transitions API */}
436
+ ```
437
+ Modifier-clicks (ctrl/cmd/shift/alt) fall back to native browser navigation.
298
438
 
299
- // Type-safe navigation
300
- import { routes } from "../route-types.gen.ts";
301
- routes["/blog/:id"]({ id: "123" }); // → "/blog/123"
302
- routes["/does-not-exist"](); // ✗ TypeScript error
439
+ ### `<Form method action?>`
440
+ Fetch-based submission that re-runs the current route's loader after the action.
441
+ ```tsx
442
+ <Form method="post">
443
+ <input name="title" />
444
+ <button type="submit">Create</button>
445
+ </Form>
446
+ <Form method="post" action="/blog/new">…</Form>
303
447
  ```
448
+ Submits as `multipart/form-data` with the `X-BractJS-Action` header (CSRF gate). If the action returns a redirect, the form follows it.
449
+
450
+ ### `<Scripts />` and `<LiveReload />`
451
+ Markers used inside `root.tsx` (§3). `<Scripts />` is where the client bundle + bootstrap data go; `<LiveReload />` is the dev-only HMR client.
452
+
453
+ ### `<Image />`
454
+ Responsive, format-converted images via the built-in `/_image` endpoint — see §20.
304
455
 
305
456
  ---
306
457
 
307
- ## Server Actions & Client Components
458
+ ## 11. Server Actions & Client Components
308
459
 
309
460
  ### `"use server"` — Server Actions
310
461
 
311
- Add `"use server"` as the first line of a file to mark its exports as Server Actions. On the client, calls are automatically serialized to `POST /_action?id=<hash>`. On the server, the real function runs.
462
+ Mark a file's exports as server actions by making `"use server"` the first line. On the **client**, imports become `fetch("/_action?id=<hash>")` proxies; on the **server**, the real function runs. Action IDs are `SHA-256(appDir-relative-path + "#" + name)`.
312
463
 
313
464
  ```ts
314
465
  // app/actions.ts
@@ -326,7 +477,7 @@ export async function deletePost(id: string) {
326
477
  ```
327
478
 
328
479
  ```tsx
329
- // app/routes/new.tsx — import used normally; on client it becomes a fetch proxy
480
+ // app/routes/new.tsx — import as normal; the client bundle gets a fetch proxy
330
481
  import { createPost } from "../actions.ts";
331
482
 
332
483
  export default function NewPost() {
@@ -339,11 +490,15 @@ export default function NewPost() {
339
490
  }
340
491
  ```
341
492
 
342
- Server actions accept `FormData` (sent as `multipart/form-data`) or JSON-serializable argument arrays. Unknown action IDs return 404 — there is no way to call a function that was not registered at startup.
493
+ - Accepts a single `FormData` (sent as `multipart/form-data`) **or** a JSON-serializable argument array.
494
+ - Unknown action IDs return 404 — only functions registered at startup are callable.
495
+ - Bodies are size-capped (1 MiB JSON) and prototype-pollution scanned.
496
+
497
+ **Streaming server actions** (async generators) are consumed with `useFetcher({ stream: true })` (§9) over `/_stream`. The endpoint requires the `X-BractJS-Action` header (set automatically by the client).
343
498
 
344
499
  ### `"use client"` — Client-Only Components
345
500
 
346
- Add `"use client"` to mark a component as browser-only. During server builds the module is stubbed to `null` to prevent browser API (`window`, `document`, `localStorage`) crashes.
501
+ Mark a component browser-only. During server builds the module is stubbed to `null` to prevent `window`/`document`/`localStorage` crashes during SSR.
347
502
 
348
503
  ```tsx
349
504
  // app/components/Counter.tsx
@@ -358,53 +513,148 @@ export function Counter() {
358
513
 
359
514
  ---
360
515
 
361
- ## Middleware
516
+ ## 12. Typed API routes
517
+
518
+ Define type-safe JSON endpoints under `/api/*` with `route`, and call them with a fully-typed `createClient`.
519
+
520
+ ### Define routes with `route(method, path, handler)`
521
+
522
+ ```ts
523
+ // app/api/users.ts
524
+ import { route } from "@bractjs/bractjs";
525
+ import { db } from "../db.server.ts";
526
+
527
+ export const listUsers = route("GET", "/api/users", async () => {
528
+ return db.users.findAll();
529
+ });
530
+
531
+ export const createUser = route("POST", "/api/users", async (input: { name: string }) => {
532
+ return db.users.create(input);
533
+ });
534
+ ```
535
+
536
+ - `GET`/`DELETE`: no body parsed. `POST`/`PUT`/`PATCH`: JSON or form body parsed into `input`.
537
+ - Bodies are capped at 1 MiB. Errors return a generic 500 in production (full message in dev).
538
+ - Handlers also receive the raw `Request` as the 2nd arg.
539
+ - `:param` segments match any non-empty value; **read and validate params from `request.url` yourself** (they aren't injected into `input`).
540
+
541
+ ### Call them with `createClient<AppApiRoutes>()`
542
+
543
+ ```ts
544
+ import { createClient } from "@bractjs/bractjs";
545
+ import type { AppApiRoutes } from "@bractjs/bractjs"; // union of your route defs
546
+
547
+ const client = createClient<AppApiRoutes>(); // optional baseUrl arg
548
+ const users = await client["/api/users"].GET(); // typed output
549
+ await client["/api/users"].POST({ name: "Alice" }); // typed input
550
+ ```
551
+
552
+ The proxy builds `METHOD path` from the property chain. Non-2xx responses throw an `Error` with `.status` and `.response` attached.
553
+
554
+ ---
555
+
556
+ ## 13. Input validation: `validate`
557
+
558
+ Validate `FormData` or a plain object against any **Zod- or Valibot-compatible** schema (anything with `.safeParse()` or `.parse()`).
559
+
560
+ ```ts
561
+ import { validate } from "@bractjs/bractjs";
562
+ import { z } from "zod";
563
+
564
+ const Schema = z.object({ title: z.string().min(1), tags: z.array(z.string()) });
565
+
566
+ export async function action({ formData }: ActionArgs) {
567
+ // Throws a 400 Response with { errors: { field: [msgs] } } on failure.
568
+ const data = await validate(Schema, formData);
569
+ await db.post.create(data); // data is fully typed + coerced
570
+ }
571
+ ```
572
+
573
+ - Repeated `FormData` keys become arrays automatically.
574
+ - On failure it throws a `Response.json({ errors }, { status: 400 })`. The exported `ValidationError` type and `FieldErrors` shape describe the structure.
575
+
576
+ ---
577
+
578
+ ## 14. Middleware
362
579
 
363
- Middleware runs before routing. Register on the module-level `pipeline` singleton.
580
+ Middleware runs **before routing** on the module-level `pipeline` singleton. Each middleware gets `(ctx, next)` and returns a `Response`. `ctx.context` is threaded into every loader/action.
364
581
 
365
582
  ```ts
366
- import { pipeline, requestLogger, cors, authGuard } from "@bractjs/bractjs";
583
+ import { pipeline, requestLogger, cors, authGuard, csp } from "@bractjs/bractjs";
584
+ import type { MiddlewareFn } from "@bractjs/bractjs";
367
585
 
368
586
  pipeline
369
587
  .use(requestLogger())
370
588
  .use(cors({ origin: "https://myapp.com" }))
589
+ .use(csp())
371
590
  .use(authGuard({ session }));
372
591
  ```
373
592
 
374
- | Middleware | Description |
593
+ ### Built-in middleware
594
+
595
+ | Middleware | What it does |
375
596
  |---|---|
376
- | `requestLogger()` | Logs method, path, status, duration |
377
- | `cors(options)` | Sets CORS headers, handles `OPTIONS` preflight |
378
- | `authGuard(options)` | Reads session, attaches `context.user`, returns 401 if unauthenticated |
597
+ | `requestLogger()` | Logs `[METHOD] /path status in Xms`. Never logs the query string or headers (token-leak safe). |
598
+ | `cors(options)` | Sets CORS headers, handles `OPTIONS` preflight (204), always sets `Vary: Origin`, refuses `credentials:true` + `origin:"*"`. |
599
+ | `authGuard(options)` | Reads the session, sets `ctx.context.user`; with `required:true` returns 401 when unauthenticated. |
600
+ | `csp(options?)` | Opt-in nonce-based Content-Security-Policy (see below). |
379
601
 
380
- **Custom middleware:**
602
+ **`cors(options)`** — `{ origin: string | string[]; methods?: string[]; credentials?: boolean }`:
603
+ ```ts
604
+ pipeline.use(cors({ origin: ["https://a.com", "https://b.com"], credentials: true }));
605
+ ```
606
+
607
+ **`authGuard(options)`** — `{ session: SessionStorageLike; required?: boolean }`:
608
+ ```ts
609
+ pipeline.use(authGuard({ session, required: true })); // 401 if no session.user
610
+ ```
611
+
612
+ **`csp(options?)`** — generates a per-request nonce, applies it to the scripts BractJS injects (via `renderToReadableStream`'s `nonce`), and sets the CSP header:
613
+ ```ts
614
+ pipeline.use(csp({
615
+ directives: { "img-src": "'self' data: https://cdn.example", "frame-ancestors": "'none'" },
616
+ reportOnly: false, // true → Content-Security-Policy-Report-Only
617
+ }));
618
+ ```
619
+ Read the nonce inside a component/middleware with `getCspNonce(context)` (key: `CSP_NONCE_KEY`) to nonce your own inline scripts.
620
+
621
+ ### Custom middleware
381
622
 
382
623
  ```ts
383
624
  import type { MiddlewareFn } from "@bractjs/bractjs";
384
625
 
385
626
  const trace: MiddlewareFn = async (ctx, next) => {
386
627
  ctx.context.requestId = crypto.randomUUID();
387
- return next();
628
+ const res = await next();
629
+ res.headers.set("X-Request-Id", ctx.context.requestId as string);
630
+ return res;
388
631
  };
632
+ pipeline.use(trace);
389
633
  ```
390
634
 
391
- `ctx.context` is threaded into every `loader` and `action` as the `context` argument.
635
+ You can also construct an isolated `new MiddlewarePipeline()` and `.run(ctx, handler)` it yourself (used internally and in tests).
392
636
 
393
637
  ---
394
638
 
395
- ## Sessions
639
+ ## 15. Sessions
640
+
641
+ Signed, tamper-proof cookie sessions (HMAC-SHA256, constant-time verify, secret rotation).
396
642
 
397
643
  ```ts
398
644
  import { createCookieSession } from "@bractjs/bractjs";
399
645
 
400
646
  const session = createCookieSession({
401
647
  name: "__session",
402
- secrets: [Bun.env.SESSION_SECRET], // rotate: prepend new secret, keep old ones
403
- maxAge: 60 * 60 * 24 * 7, // 1 week
404
- secure: true,
405
- sameSite: "lax",
648
+ secrets: [Bun.env.SESSION_SECRET!], // first signs; all verify (rotate by prepending)
649
+ maxAge: 60 * 60 * 24 * 7, // seconds; 1 week
650
+ secure: true, // false only for local HTTP dev
651
+ sameSite: "Lax", // "Strict" | "Lax" | "None"
406
652
  });
653
+ ```
407
654
 
655
+ Read in a loader, write in an action:
656
+
657
+ ```ts
408
658
  export async function loader({ request }: LoaderArgs) {
409
659
  const s = await session.getSession(request.headers.get("Cookie"));
410
660
  return { user: s.get("user") };
@@ -412,406 +662,464 @@ export async function loader({ request }: LoaderArgs) {
412
662
 
413
663
  export async function action({ request }: ActionArgs) {
414
664
  const s = await session.getSession(request.headers.get("Cookie"));
415
- s.set("user", { id: 1, name: "Alice" });
665
+ s.set("user", { id: 1, name: "Alice" }); // also: s.get, s.has, s.delete
416
666
  return redirect("/dashboard", {
417
- headers: { "Set-Cookie": await session.commitSession(s) },
667
+ headers: { "Set-Cookie": await session.commitSession(s) }, // opt: { maxAge }
418
668
  });
419
669
  }
420
670
  ```
421
671
 
422
- Cookies are signed with HMAC-SHA256 using `crypto.subtle`. Tampered cookies are silently rejected and return an empty session.
423
-
424
- > Generate a secret: `openssl rand -base64 32`
672
+ - Each secret must be ≥16 chars; `secrets` must be non-empty (throws otherwise).
673
+ - Tampered cookies are silently rejected → empty session.
674
+ - Generate a secret: `openssl rand -base64 32`.
425
675
 
426
676
  ---
427
677
 
428
- ## Environment Variables
678
+ ## 16. Lifecycle hooks: `defineLifecycle`
429
679
 
430
- | Convention | Behavior |
431
- |---|---|
432
- | `*.server.ts` / `*.server.tsx` | Import blocked in client bundles at build time — hard error |
433
- | Keys in `clientEnv` | Replaced with string literals in client bundle |
434
- | All other `process.env.*` | Become `"undefined"` in client bundles |
435
-
436
- ```ts
437
- // db.server.ts — never reaches the browser
438
- export const db = new Database(Bun.env.DATABASE_URL);
439
- ```
440
-
441
- ---
442
-
443
- ## Server Lifecycle Hooks
444
-
445
- Use `defineLifecycle()` in `app/lifecycle.ts` to run code when the server starts, shuts down, or encounters an error. The shutdown hook runs on **any** exit signal (`SIGTERM`, `SIGINT`, `SIGUSR2`, `beforeExit`, and uncaught exceptions), so database connections are always closed cleanly.
680
+ Run code on server start, shutdown, and unexpected errors. Shutdown fires on **any** exit signal (`SIGTERM`, `SIGINT`, `SIGUSR2`, `beforeExit`, uncaught exceptions).
446
681
 
447
682
  ```ts
448
683
  // app/lifecycle.ts
449
684
  import { defineLifecycle } from "@bractjs/bractjs";
450
685
  import { db } from "./db.server.ts";
451
- import * as Sentry from "@sentry/bun";
452
686
 
453
687
  export default defineLifecycle({
454
- async onStart() {
455
- await db.connect();
456
- console.log("Database connected");
457
- },
458
- async onShutdown() {
459
- await db.disconnect();
460
- console.log("Database disconnected");
461
- },
462
- async onError(err, request) {
463
- // request is undefined for process-level uncaught exceptions
464
- Sentry.captureException(err, {
465
- extra: { url: request?.url },
466
- });
688
+ async onStart() { await db.connect(); },
689
+ async onShutdown(){ await db.disconnect(); },
690
+ onError(err, request) {
691
+ Sentry.captureException(err, { extra: { url: request?.url } });
467
692
  },
468
693
  });
469
694
  ```
470
695
 
471
- BractJS picks up `app/lifecycle.ts` automatically in dev mode. In production, spread the hooks into `createServer()`:
696
+ | Hook | When |
697
+ |------|------|
698
+ | `onStart` | Once, after the server starts listening. |
699
+ | `onShutdown` | Before exit — any signal, programmatic `stop()`, or uncaught exception. |
700
+ | `onError` | Every unexpected error (loader/action throws, uncaught exceptions). Redirects and `HttpError`s are intentional control flow and are **not** reported. `request` is `undefined` for process-level exceptions. |
472
701
 
473
- ```ts
474
- // server.ts (production entry)
475
- import { createServer } from "@bractjs/bractjs";
476
- import lifecycle from "./app/lifecycle.ts";
702
+ - **Dev** picks up `app/lifecycle.ts` automatically.
703
+ - **Production**: spread into `createServer()`:
704
+ ```ts
705
+ import { createServer } from "@bractjs/bractjs";
706
+ import lifecycle from "./app/lifecycle.ts";
707
+ createServer({ port: 3000, ...lifecycle });
708
+ ```
477
709
 
478
- createServer({ port: 3000, ...lifecycle });
479
- ```
710
+ `createServer()` returns `{ stop }`. `stop()` runs `onShutdown` and closes the listener but does **not** call `process.exit()` (good for tests/supervisors). Signals do exit.
480
711
 
481
- | Hook | When it runs |
482
- |------|-------------|
483
- | `onStart` | Once, after the server begins accepting requests |
484
- | `onShutdown` | Before process exit — any signal, programmatic `stop()`, or uncaught exception |
485
- | `onError` | Every unexpected error: loader failures, action throws, uncaught exceptions. Redirects and `HttpError` throws are intentional control flow and are **not** reported. |
712
+ ---
486
713
 
487
- ### Programmatic stop vs signal-driven termination
714
+ ## 17. Environment variables
488
715
 
489
- `createServer()` returns a `{ stop }` handle:
716
+ | Convention | Behavior |
717
+ |---|---|
718
+ | `*.server.ts` / `*.server.tsx` | **Stubbed out of the client bundle.** Import it freely from a route's `loader`/`action`; every export is replaced by an inert stub in the browser build, so the real source (DB drivers, secrets, `bun:sqlite`) never ships. The stub throws if you accidentally call it on the client. |
719
+ | Keys listed in `clientEnv` | Replaced with string literals in the client bundle. |
720
+ | Any other `process.env.*` | Becomes the literal `"undefined"` in the client bundle. |
490
721
 
491
722
  ```ts
492
- const srv = createServer({ port: 3000 });
493
- // later:
494
- srv.stop(); // runs onShutdown, closes the listener, does NOT exit the process
723
+ // db.server.ts never reaches the browser
724
+ import { Database } from "bun:sqlite";
725
+ export const db = new Database(Bun.env.DATABASE_URL!);
495
726
  ```
496
727
 
497
- `stop()` returns the process to a normal idle state — useful in tests, integration harnesses, or any parent that wants to manage its own lifecycle. It does **not** call `process.exit()`.
728
+ ```ts
729
+ // app/routes/posts.tsx — import the server module inside the loader
730
+ import { db } from "../db.server.ts"; // stubbed in the client bundle
498
731
 
499
- The full termination path (`process.exit(0)`) only fires when a signal handler picks up the shutdown: `SIGTERM`, `SIGINT`, `SIGUSR2`, or `uncaughtException`. If you want a programmatic stop to terminate the process, call `process.exit(0)` yourself after `stop()` returns.
732
+ export async function loader() {
733
+ return { posts: db.query("SELECT * FROM posts").all() };
734
+ }
735
+ ```
500
736
 
501
- ---
737
+ > BractJS ships the whole route module — `loader` and `action` included — to the client, so a server import is reachable from the client graph. The `serverModuleStubPlugin` (applied automatically by `bractjs dev`/`build`) replaces every `*.server.ts` export with a throwing stub: the import resolves, the loader/action are dead code on the client, and **zero** server source is emitted. The stricter, hard-failing `serverOnlyPlugin` is still exported if you'd rather a server import be a build error.
502
738
 
503
- ## Configuration Reference
739
+ ```ts
740
+ // bractjs.config.ts
741
+ export default { clientEnv: ["PUBLIC_API_URL"] };
742
+ ```
504
743
 
505
- All fields are optional. BractJS works with zero configuration.
744
+ ```ts
745
+ // in a client component — only allow-listed keys survive
746
+ fetch(`${process.env.PUBLIC_API_URL}/items`);
747
+ ```
506
748
 
507
- | Field | Type | Default | Description |
508
- |-------|------|---------|-------------|
509
- | `port` | `number` | `3000` | TCP port |
510
- | `appDir` | `string` | `"./app"` | Directory containing `routes/` and `root.tsx` |
511
- | `publicDir` | `string` | `"./public"` | Static assets (served with no-cache) |
512
- | `buildDir` | `string` | `"./build"` | Output for `bractjs build` |
513
- | `imageCacheDir` | `string` | `".bract-image-cache"` | Disk cache for optimized images |
514
- | `sourcemap` | `string` | `"external"` | `"none"` \| `"inline"` \| `"external"` |
515
- | `minify` | `boolean` | `true` | Minify client bundles |
516
- | `clientEnv` | `string[]` | `[]` | `process.env` keys exposed to the client |
517
- | `onStart` | `() => void \| Promise<void>` | — | Called once after the server starts listening |
518
- | `onShutdown` | `() => void \| Promise<void>` | — | Called before process exit on any signal |
519
- | `onError` | `(err, request?) => void \| Promise<void>` | — | Called on every unexpected error; `request` is `undefined` for process-level exceptions |
749
+ On the server, read env via `Bun.env.*` directly.
520
750
 
521
751
  ---
522
752
 
523
- ## CLI
753
+ ## 18. Typed routes
524
754
 
525
- | Command | Description |
526
- |---------|-------------|
527
- | `bractjs new <name>` | Scaffold a new app into `<name>/` |
528
- | `bractjs dev` | Start dev server with HMR on port 3000 |
529
- | `bractjs build` | Dual server + client build with content-hashed output |
530
- | `bractjs start` | Serve the production build |
531
- | `bractjs codegen [app] [out]` | Generate typed route types into `app/route-types.gen.ts` |
532
- | `bractjs codegen:registry [app]` | Generate `app/_generated/{routes,actions}.ts` (single-binary prep) |
533
- | `bractjs codegen:manifest [app] [build]` | Snapshot `route-manifest.json` into `app/_generated/manifest.ts` |
534
- | `bractjs compile [outfile] [entry]` | Full single-binary pipeline (codegen → build → compile) |
755
+ Generate type-safe routing from your route files — one command wires `<Link>`, `useNavigate`, `useParams`, and `useSearchParams` to your actual routes.
535
756
 
536
- The CLI is a thin convenience layer. Every command delegates to a public programmatic API — you can call the same functions directly from your own scripts without the CLI.
757
+ ```sh
758
+ bractjs codegen # ./app → ./app/route-types.gen.ts
759
+ bractjs codegen ./app ./app/types.ts # explicit paths
760
+ ```
537
761
 
538
- ---
762
+ Runs automatically during `bractjs build`. Make sure the generated file is part of your TypeScript program (it is, if your `tsconfig.json` `include`s `app/`). It augments BractJS's `Register` interface, after which the runtime components and hooks become type-safe — **no per-route imports needed**:
539
763
 
540
- ## Single-Binary Deployment (`bun build --compile`)
764
+ ```tsx
765
+ <Link to="/blog/:id" params={{ id }} /> // ✅ "/blog/:id" autocompletes; params typed
766
+ <Link to="/blgo/:id" params={{ id }} /> // ❌ typo'd route — compile error
767
+ <Link to="/blog/:id" params={{ x: id }} /> // ❌ wrong param key — compile error
541
768
 
542
- BractJS can be packaged as a single executable using Bun's `--compile` flag. Because `bun build --compile` can't trace runtime filesystem scans or dynamic `import(absPath)` calls, BractJS provides a codegen step that materialises every route, layout, and server action into static imports. The compiled binary has zero filesystem dependence at startup.
769
+ const navigate = useNavigate();
770
+ navigate("/blog/:id", { params: { id } }); // ✅ same typing as <Link>
543
771
 
544
- ### One-shot
772
+ const { id } = useParams<"/blog/:id">(); // id: string
773
+ ```
545
774
 
546
- ```sh
547
- bractjs compile ./myapp
548
- # Equivalent to:
549
- # bractjs codegen:registry # writes app/_generated/{routes,actions}.ts
550
- # bractjs build # writes build/client/* + route-manifest.json
551
- # bractjs codegen:manifest # snapshots manifest app/_generated/manifest.ts
552
- # bun build --compile app/server.ts --outfile ./myapp
775
+ Building the URL yourself (`<Link to={`/blog/${id}`}>`) still type-checks, so adopting codegen never breaks existing links.
776
+
777
+ The generated file also exports types/helpers for typed loaders and explicit URL building:
778
+
779
+ ```ts
780
+ import type { TypedLoaderArgs } from "../route-types.gen.ts";
781
+ import { routes } from "../route-types.gen.ts";
782
+
783
+ export async function loader({ params }: TypedLoaderArgs<"/blog/:id">) {
784
+ return db.post.findById(params.id); // params.id: string
785
+ }
786
+ routes["/blog/:id"]({ id: "123" }); // → "/blog/123" (typo'd routes won't compile)
553
787
  ```
554
788
 
555
- ### Manual pipeline (custom build step)
789
+ **Type a route's search params or context** by augmenting the package interfaces — `SearchParams<T>` / `Context<T>` and `useSearchParams<T>()` pick it up:
556
790
 
557
- ```sh
558
- bractjs codegen:registry # A — scan routes/actions
559
- bractjs build # B client + server bundles
560
- bractjs codegen:manifest # C embed manifest as a TS constant
561
- bun build --compile app/server.ts \ # D — single binary
562
- --asset build/client/ \ # (embeds JS/CSS into the binary)
563
- --outfile ./myapp
791
+ ```ts
792
+ declare module "@bractjs/bractjs" {
793
+ interface RouteSearchParamsMap { "/blog": { page: string; sort: string } }
794
+ interface RouteContextMap { "/admin": { user: { id: string; role: "admin" } } }
795
+ }
564
796
  ```
565
797
 
566
- Asset embedding (`--asset build/client/`) is optional. Without it you ship `myapp` + the `build/client/` folder side-by-side. With it, you get a true single file.
798
+ You can also call `writeRouteTypes(appDir, outPath?)` / `generateRouteTypes(appDir)` programmatically.
567
799
 
568
- ### The `app/server.ts` entry
800
+ > **Heads up:** earlier versions documented `useParams<RouteParams<"/blog/:id">>()` and untyped `<Link to={string}>`. The route-literal form (`useParams<"/blog/:id">()`) and typed `<Link>`/`useNavigate` are the current API; the old forms still compile.
569
801
 
570
- The scaffold template (`bractjs new`) includes `app/server.ts`:
802
+ ---
803
+
804
+ ## 19. Internationalization utilities
805
+
806
+ BractJS exports **utilities** for locale-prefixed routing. These are helpers you wire up yourself (there is no fully-automatic locale router yet) plus the `useLocale` / `useLocalizedLink` client hooks (§9).
571
807
 
572
808
  ```ts
573
- import { createServer } from "@bractjs/bractjs";
574
- import { routeFiles, moduleRegistry } from "./_generated/routes.ts";
575
- import { actionModules } from "./_generated/actions.ts";
576
- import { manifest } from "./_generated/manifest.ts";
809
+ import { wrapRoutesWithLocale, stripLocale, localizedDataPath } from "@bractjs/bractjs";
810
+ import type { I18nConfig } from "@bractjs/bractjs";
577
811
 
578
- createServer({
579
- port: Number(process.env.PORT ?? 3000),
580
- appDir: "./app",
581
- publicDir: "./public",
582
- manifest,
583
- routeFiles, // skips Bun.Glob route scan
584
- moduleRegistry, // skips dynamic import(absPath) of route modules
585
- actionModules, // skips Bun.Glob + dynamic import for "use server" files
586
- });
812
+ const i18n: I18nConfig = { locales: ["en", "fr"], defaultLocale: "en" };
813
+
814
+ // Add /:locale-prefixed variants alongside the originals.
815
+ const localized = wrapRoutesWithLocale(routeFiles, i18n);
816
+
817
+ // Split a locale off a pathname.
818
+ const { locale, strippedPathname } = stripLocale("/fr/about", i18n.locales);
819
+ // { locale: "fr", strippedPathname: "/about" }
820
+
821
+ // Build a locale-aware data path.
822
+ localizedDataPath("/about", "fr"); // → "/fr/about"
587
823
  ```
588
824
 
589
- When all four of `manifest`, `routeFiles`, `moduleRegistry`, and `actionModules` are present, the server boots with **no filesystem reads of `appDir`** — the routing trie, layout chain, server-action registry, and asset manifest all come from the pre-imported modules.
825
+ On the client, read the active locale and build localized links:
590
826
 
591
- ### Custom client builds — required plugins
827
+ ```tsx
828
+ const locale = useLocale("en");
829
+ const to = useLocalizedLink("en");
830
+ <Link to={to("/about")} />; // → /en/about
831
+ ```
592
832
 
593
- If you write your own `Bun.build()` call (instead of running `bractjs build`), you MUST apply these plugins or face crashes / secret leaks:
833
+ ---
594
834
 
595
- | Bundle | Plugin | What breaks without it |
596
- |---|---|---|
597
- | Server | `useClientStubPlugin` | Server binary crashes when React tries to invoke browser-only hooks/APIs from `"use client"` modules |
598
- | Client | `createUseServerProxyPlugin(appDir)` | Server-action bodies (DB queries, secrets) ship inside the browser JS |
599
- | Client | `serverOnlyPlugin` | Imports of `*.server.ts` leak into the client bundle |
600
- | Client | `clientEnvPlugin(allowedKeys, env)` | Server env vars leak into the browser bundle |
601
- | Client | `cssModulesPlugin` | `*.module.css` imports don't resolve |
835
+ ## 20. Image optimization
602
836
 
603
- ```ts
604
- import {
605
- useClientStubPlugin,
606
- createUseServerProxyPlugin,
607
- serverOnlyPlugin,
608
- clientEnvPlugin,
609
- cssModulesPlugin,
610
- } from "@bractjs/bractjs";
837
+ `<Image>` serves responsive, format-converted images through the built-in `/_image` endpoint. Requires [ImageMagick](https://imagemagick.org) (`magick` or `convert`) — without it, originals are served as-is.
611
838
 
612
- // Server bundle (target: "bun"):
613
- plugins: [useClientStubPlugin];
839
+ ```tsx
840
+ import { Image } from "@bractjs/bractjs";
614
841
 
615
- // Client bundle (target: "browser"):
616
- plugins: [
617
- serverOnlyPlugin,
618
- createUseServerProxyPlugin("./app"),
619
- clientEnvPlugin(["PUBLIC_API_URL"], Bun.env as Record<string, string>),
620
- cssModulesPlugin,
621
- ];
842
+ <Image src="/public/hero.jpg" alt="Hero" width={1200} height={600} />
843
+
844
+ {/* above-the-fold: eager + fetchpriority=high */}
845
+ <Image src="/public/hero.jpg" alt="Hero" width={1200} priority />
846
+
847
+ <Image
848
+ src="/public/photo.jpg" alt="Photo" width={800}
849
+ format="avif" quality={70} fit="contain"
850
+ sizes="(max-width: 640px) 100vw, 50vw"
851
+ />
622
852
  ```
623
853
 
624
- The `createUseServerProxyPlugin(appDir)` factory exists because server-action IDs are SHA-256 hashes of the appDir-relative path. If the server and client compute different paths (e.g. CI vs prod), every `/_action?id=...` returns 404. Always pass the same `appDir` you pass to `createServer`.
854
+ | Prop | Type | Default | Notes |
855
+ |------|------|---------|-------|
856
+ | `src` | `string` | — | Path under `/public/` (required) |
857
+ | `alt` | `string` | — | Required |
858
+ | `width` / `height` | `number` | — | Intrinsic size |
859
+ | `quality` | `number` | `80` | 1–100 |
860
+ | `format` | `"webp" \| "avif" \| "jpeg" \| "png"` | `"webp"` | `ImageFormat` |
861
+ | `fit` | `"cover" \| "contain" \| "fill"` | `"cover"` | `ImageFit` |
862
+ | `priority` | `boolean` | `false` | Disable lazy load, set `fetchpriority=high` |
863
+ | `sizes` | `string` | `"100vw"` | HTML `sizes` |
864
+
865
+ Generates a `srcset` across breakpoints (320→1920px). Optimized images are cached in memory (LRU, 200 slots) and on disk (`.bract-image-cache/`, survives restarts), both served `Cache-Control: immutable`. The endpoint validates widths against an allowlist, caps total pixel area, limits concurrency, and kills runaway transforms (DoS hardening).
866
+
867
+ `ImageProps`, `ImageFormat`, and `ImageFit` are exported types.
625
868
 
626
869
  ---
627
870
 
628
- ## Programmatic API
871
+ ## 21. Build & run
629
872
 
630
- All three runtime operations are importable, so BractJS can be embedded in existing servers or custom build scripts without the CLI.
873
+ ### CLI
631
874
 
632
- ### `createDevServer(options?)`
875
+ | Command | Description |
876
+ |---------|-------------|
877
+ | `bractjs new <name>` | Scaffold a new app into `<name>/`. |
878
+ | `bractjs dev` | Dev server with HMR (port 3000, HMR ws 3001). |
879
+ | `bractjs build` | Dual server + client build with content-hashed output. |
880
+ | `bractjs start` | Serve the production build. |
881
+ | `bractjs codegen [app] [out]` | Generate `route-types.gen.ts`. |
882
+ | `bractjs codegen:registry [app]` | Generate `app/_generated/{routes,actions}.ts`. |
883
+ | `bractjs codegen:manifest [app] [build]` | Snapshot manifest → `app/_generated/manifest.ts`. |
884
+ | `bractjs compile [outfile] [entry]` | Full single-binary pipeline. |
633
885
 
886
+ The CLI is a thin wrapper — every command delegates to a public function, so you can script the same thing.
887
+
888
+ ### Programmatic API
889
+
890
+ **`createDevServer(options?)`** — dev server with HMR.
634
891
  ```ts
635
892
  import { createDevServer } from "@bractjs/bractjs";
636
893
 
637
894
  const dev = await createDevServer({
638
- port: 3000, // HTTP port (default: config.port ?? 3000)
639
- hmrPort: 3001, // HMR WebSocket port (default: 3001)
895
+ port: 3000, // default: config.port ?? 3000
896
+ hmrPort: 3001, // HMR websocket
640
897
  config: { appDir: "./app", clientEnv: ["PUBLIC_API_URL"] },
641
- skipUserConfig: false, // set true to skip loading bractjs.config.ts from cwd
898
+ skipUserConfig: false, // true don't read bractjs.config.ts
642
899
  });
643
-
644
- // Later — stops the HTTP server and HMR WebSocket server
645
900
  dev.stop();
646
901
  ```
647
902
 
648
- ### `runBuild(config?)`
649
-
903
+ **`runBuild(config?)`** — production build (accepts only build-relevant fields).
650
904
  ```ts
651
905
  import { runBuild } from "@bractjs/bractjs";
652
906
 
653
907
  await runBuild({
654
908
  appDir: "./app",
655
- buildDir: "./dist",
909
+ buildDir: "./build",
656
910
  minify: true,
657
- sourcemap: "external",
911
+ sourcemap: "external", // "none" | "linked" | "inline" | "external"
658
912
  clientEnv: ["PUBLIC_API_URL"],
913
+ plugins: [], // extra Bun plugins
659
914
  });
660
915
  ```
661
916
 
662
- `runBuild` only accepts build-relevant fields (`appDir`, `buildDir`, `sourcemap`, `minify`, `clientEnv`, `plugins`). Server-only fields like `port`, `manifest`, and `publicDir` are not accepted — this makes it safe to call from a build script without constructing a full server config.
917
+ **`loadUserConfig()`** read `bractjs.config.ts` (or `.js`) from cwd, validated.
918
+ ```ts
919
+ import { loadUserConfig } from "@bractjs/bractjs";
920
+ const cfg = await loadUserConfig(); // {} if no file; throws on a malformed shape
921
+ ```
663
922
 
664
- ### `loadUserConfig()`
923
+ **`createServer(config?)`** — production HTTP server. Returns `{ stop }`.
924
+ ```ts
925
+ import { createServer } from "@bractjs/bractjs";
926
+ import lifecycle from "./app/lifecycle.ts";
927
+ const srv = createServer({ port: 3000, buildDir: "./build", ...lifecycle });
928
+ ```
665
929
 
930
+ **`buildFetchHandler(config)`** — the adapter-agnostic `(Request) => Promise<Response>` core, if you want to mount BractJS inside another server.
666
931
  ```ts
667
- import { loadUserConfig } from "@bractjs/bractjs";
932
+ import { buildFetchHandler } from "@bractjs/bractjs";
933
+ const handler = buildFetchHandler({ appDir: "./app", manifest });
934
+ Bun.serve({ port: 3000, fetch: handler });
935
+ ```
936
+
937
+ `renderRoute(options)` (low-level SSR render) and the `RenderOptions`/`ServerManifest`/`BractJSConfig` types are also exported for advanced embedding.
938
+
939
+ ---
940
+
941
+ ## 22. Single-binary deployment (`bun build --compile`)
668
942
 
669
- const cfg = await loadUserConfig();
670
- // Reads bractjs.config.ts (or .js) from process.cwd()
671
- // Returns {} if no config file exists
943
+ BractJS compiles to a single executable. Because `bun build --compile` can't trace runtime fs scans or dynamic `import(absPath)`, a codegen step materializes routes, layouts, actions, and the manifest into static imports so the binary boots with **zero filesystem reads of `appDir`**.
944
+
945
+ ### One-shot
946
+
947
+ ```sh
948
+ bractjs compile ./myapp
949
+ # = codegen:registry → build → codegen:manifest → bun build --compile app/server.ts
672
950
  ```
673
951
 
674
- ### `createServer(config?)` (production)
952
+ ### The `app/server.ts` entry
675
953
 
676
- Already exported. Starts the production HTTP server directly:
954
+ The scaffold includes:
677
955
 
678
956
  ```ts
679
957
  import { createServer } from "@bractjs/bractjs";
680
- import lifecycle from "./app/lifecycle.ts";
958
+ import { routeFiles, moduleRegistry } from "./_generated/routes.ts";
959
+ import { actionModules } from "./_generated/actions.ts";
960
+ import { manifest } from "./_generated/manifest.ts";
681
961
 
682
- createServer({ port: 3000, buildDir: "./build", ...lifecycle });
962
+ createServer({
963
+ port: Number(process.env.PORT ?? 3000),
964
+ appDir: "./app",
965
+ publicDir: "./public",
966
+ manifest, // no manifest read from disk
967
+ routeFiles, // no Bun.Glob route scan
968
+ moduleRegistry, // no dynamic import(absPath)
969
+ actionModules, // no scan/import for "use server" files
970
+ });
683
971
  ```
684
972
 
685
- ---
973
+ When all four are present, the server uses the pre-imported modules for routing, layouts, actions, and assets.
686
974
 
687
- ## App Directory Structure
975
+ ### Manual pipeline
688
976
 
689
- ```
690
- my-app/
691
- ├── app/
692
- │ ├── root.tsx # required<html> shell
693
- │ ├── server.ts # bun build --compile entrypoint (single-binary build)
694
- │ ├── lifecycle.ts # optional — onStart / onShutdown / onError hooks
695
- │ ├── route-types.gen.ts # generated by bractjs codegen
696
- │ ├── _generated/ # generated by bractjs codegen:registry / codegen:manifest
697
- │ │ ├── routes.ts # static imports for routes + layouts + root
698
- │ │ ├── actions.ts # static imports for "use server" modules
699
- │ │ └── manifest.ts # inline ServerManifest constant
700
- │ ├── actions.ts # "use server" actions
701
- │ └── routes/
702
- │ ├── _index.tsx # → /
703
- │ ├── about.tsx # → /about
704
- │ ├── blog/
705
- │ │ ├── layout.tsx # layout for /blog/*
706
- │ │ ├── _index.tsx # → /blog
707
- │ │ └── [id].tsx # → /blog/:id
708
- │ └── docs/
709
- │ └── [...slug].tsx # → /docs/*
710
- ├── public/
711
- │ └── favicon.ico
712
- └── build/ # generated — do not edit
713
- ├── server/
714
- ├── client/
715
- └── route-manifest.json
977
+ ```sh
978
+ bractjs codegen:registry # A — scan routes/actions → static imports
979
+ bractjs build # B — client + server bundles + manifest
980
+ bractjs codegen:manifest # Cembed manifest as a TS constant
981
+ bun build --compile app/server.ts \ # D single binary
982
+ --asset build/client/ --outfile ./myapp
716
983
  ```
717
984
 
718
- The `_generated/` directory is only required for the single-binary workflow. Regular `bractjs dev` / `bractjs build` / `bractjs start` work without it.
985
+ `--asset build/client/` embeds JS/CSS into the binary (true single file); omit it to ship `myapp` + `build/client/` side by side.
986
+
987
+ The codegen functions are exported: `writeModuleRegistries(appDir)`, `writeManifestModule(appDir, buildDir)`, and the lower-level `generateRouteRegistry` / `generateActionRegistry` / `generateManifestModule`.
988
+
989
+ > **Contributor note — keep the binary working:** anything on the server request/startup path must avoid runtime `Bun.Glob`/computed-path `import()`, must fall back from `realpath()` to `Bun.file().exists()` for embedded assets, and must preserve every consumed export when projecting route modules. Two tests enforce this: `src/__tests__/compile-safety.test.ts` (fast static scan) and `src/__tests__/compile-smoke.test.ts` (compiles and boots a real binary).
719
990
 
720
991
  ---
721
992
 
722
- ## Architecture
993
+ ## 23. Custom adapters
723
994
 
724
- ```
725
- Request
726
- └─ Middleware pipeline
727
- └─ /_action → Server Action registry → fn(...args)
728
- └─ /_image → ImageMagick transform → LRU cache → Response
729
- └─ Route trie (static > param > catch-all)
730
- └─ Layout chain (root → layout → route)
731
- └─ Parallel loaders (Promise.all)
732
- └─ renderToReadableStream → streaming Response
733
- ```
995
+ The server core is adapter-agnostic. The default is `BunAdapter` (wraps `Bun.serve`); supply your own via `createServer({ adapter })`.
734
996
 
735
- Client:
736
- ```
737
- hydrateRoot(document)
738
- └─ ClientRouter (RouterContext + NavigationContext)
739
- └─ Outlet → React.lazy route chunk
740
- └─ useLoaderData / useParams / useNavigation / …
741
- ```
997
+ ```ts
998
+ import type { BractAdapter } from "@bractjs/bractjs";
999
+ import { BunAdapter } from "@bractjs/bractjs";
742
1000
 
743
- Build pipeline (`bractjs build`):
744
- ```
745
- 1. codegen → app/route-types.gen.ts
746
- 2. server bundle → Bun.build (target: bun) + useClientStubPlugin
747
- 3. client bundle → Bun.build (target: browser, splitting) + createUseServerProxyPlugin(appDir)
748
- 4. content-hash → rename outputs, write route-manifest.json
1001
+ // BractAdapter: { fetch(req): Promise<Response>; listen?(port): void }
749
1002
  ```
750
1003
 
751
- Single-binary pipeline (`bractjs compile`):
752
- ```
753
- A. registry codegen app/_generated/{routes,actions}.ts (static imports)
754
- B. dual build → build/server/, build/client/, route-manifest.json
755
- C. manifest codegen → app/_generated/manifest.ts (inline constant)
756
- D. bun build → single executable with no runtime fs scans
757
- --compile app/server.ts [--asset build/client/]
1004
+ **Cloudflare Workers:**
1005
+ ```ts
1006
+ import { buildFetchHandler, makeCloudflareHandler } from "@bractjs/bractjs";
1007
+ const handler = buildFetchHandler({ appDir: "./app", manifest });
1008
+ export default makeCloudflareHandler(handler);
1009
+ // or createCloudflareAdapter(handler) for the BractAdapter-compatible form
758
1010
  ```
759
1011
 
760
1012
  ---
761
1013
 
762
- ## Package Structure
1014
+ ## 24. Build plugins
763
1015
 
1016
+ If you write your own `Bun.build()` (instead of `bractjs build`), you **must** apply these or face crashes / secret leaks. All are exported.
1017
+
1018
+ | Bundle | Plugin | Without it |
1019
+ |---|---|---|
1020
+ | Server | `useClientStubPlugin` | Server crashes calling browser-only hooks from `"use client"` modules. |
1021
+ | Client | `createUseServerProxyPlugin(appDir)` | Server-action bodies (DB code, secrets) ship in the browser JS. |
1022
+ | Client | `serverModuleStubPlugin` | `*.server.ts` source (DB drivers, secrets) leaks into the client bundle. |
1023
+ | Client | `clientEnvPlugin(allowedKeys, env)` | Server env vars leak into the browser bundle. |
1024
+ | Client | `cssModulesPlugin` | `*.module.css` imports don't resolve. |
1025
+
1026
+ ```ts
1027
+ import {
1028
+ useClientStubPlugin, createUseServerProxyPlugin,
1029
+ serverModuleStubPlugin, clientEnvPlugin, cssModulesPlugin,
1030
+ } from "@bractjs/bractjs";
1031
+
1032
+ // Server bundle (target: "bun"):
1033
+ plugins: [useClientStubPlugin];
1034
+
1035
+ // Client bundle (target: "browser"):
1036
+ plugins: [
1037
+ serverModuleStubPlugin,
1038
+ createUseServerProxyPlugin("./app"), // same appDir as createServer!
1039
+ clientEnvPlugin(["PUBLIC_API_URL"], Bun.env as Record<string, string>),
1040
+ cssModulesPlugin,
1041
+ ];
764
1042
  ```
765
- bractjs/
766
- ├── src/
767
- │ ├── server/ # SSR, routing, loaders, actions, sessions, action-registry
768
- │ ├── client/ # hydrateRoot, contexts, hooks, Link/Form/Image components
769
- │ ├── build/ # Bun.build orchestration, manifest, hashing, directives
770
- │ ├── codegen/ # route-types.gen.ts + module-registry codegen (_generated/*)
771
- │ ├── image/ # /_image handler, ImageMagick optimizer, LRU cache
772
- │ ├── dev/ # watcher, HMR server + client, error overlay
773
- │ ├── shared/ # types, errors, deferred, context
774
- │ └── middleware/ # requestLogger, cors, authGuard
775
- ├── bin/cli.ts
776
- ├── types/ # TypeScript declaration files
777
- └── templates/
778
- └── new-app/ # scaffold template
779
- ```
1043
+
1044
+ > Always pass the **same `appDir`** to `createUseServerProxyPlugin` that you pass to `createServer` — action IDs hash the appDir-relative path, so a mismatch makes every `/_action` return 404. `transformCssModule(filePath)` is exported for custom CSS pipelines; `useServerProxyPlugin` is the legacy absolute-path variant. `serverModuleStubPlugin` stubs `*.server.ts` exports so a route can import a server module inside its loader/action without leaking source; `serverOnlyPlugin` is the stricter predecessor that hard-fails such imports instead (still exported for opt-in use).
780
1045
 
781
1046
  ---
782
1047
 
783
- ## Why BractJS
1048
+ ## 25. Configuration reference
784
1049
 
785
- - **Bun-native** `Bun.serve`, `Bun.build`, `Bun.file`, `Bun.Glob`, `Bun.watch`. No Node.js.
786
- - **Zero framework deps** — only peer dependencies are `react` and `react-dom`.
787
- - **Streaming SSR** `renderToReadableStream()` with `defer()` for slow data.
788
- - **File-based routing** — drop a file in `app/routes/`, it's a route.
789
- - **Full-stack** loaders, actions, sessions, server actions, and middleware in one package.
790
- - **Typed routes** codegen produces per-route param types and a type-safe route builder.
1050
+ All fields optional. Put them in `bractjs.config.ts` (default export) or pass to `createServer` / `createDevServer` / `runBuild`.
1051
+
1052
+ | Field | Type | Default | Description |
1053
+ |-------|------|---------|-------------|
1054
+ | `port` | `number` | `3000` | TCP port |
1055
+ | `appDir` | `string` | `"./app"` | Contains `routes/` and `root.tsx` |
1056
+ | `publicDir` | `string` | `"./public"` | Static assets (served no-cache) |
1057
+ | `buildDir` | `string` | `"./build"` | Build output |
1058
+ | `imageCacheDir` | `string` | `".bract-image-cache"` | Optimized-image disk cache |
1059
+ | `sourcemap` | `string` | `"external"` | `"none" \| "linked" \| "inline" \| "external"` |
1060
+ | `minify` | `boolean` | `true` | Minify client bundles |
1061
+ | `clientEnv` | `string[]` | `[]` | `process.env` keys exposed to the client |
1062
+ | `plugins` | `BunPlugin[]` | `[]` | Extra client-build plugins |
1063
+ | `adapter` | `BractAdapter` | `BunAdapter` | Custom server adapter |
1064
+ | `i18n` | `I18nConfig` | — | Locale config consumed by the i18n utilities |
1065
+ | `onStart` / `onShutdown` / `onError` | hooks | — | Lifecycle (§16) |
1066
+
1067
+ `loadUserConfig()` validates these shapes and throws a clear error on an obvious mistake (e.g. a string `port`).
791
1068
 
792
1069
  ---
793
1070
 
794
- ## Status
1071
+ ## 26. Full export index
1072
+
1073
+ Everything importable from `@bractjs/bractjs` ([src/index.ts](src/index.ts)):
1074
+
1075
+ **Server / runtime:** `createServer`, `buildFetchHandler`, `renderRoute`, `redirect`, `json`, `error`, `defineContext`, `route`, `validate`, `BunAdapter`, `defineLifecycle`
1076
+
1077
+ **Errors:** `BractJSError`, `HttpError`, `isRedirect`, `isHttpError`, `isBractJSError`
1078
+
1079
+ **Streaming:** `defer`, `Deferred`, `isDeferred`, `Await`
1080
+
1081
+ **Context:** `BractJSContext`, `BractJSProvider`, `useBractJSContext`
1082
+
1083
+ **Middleware:** `pipeline`, `MiddlewarePipeline`, `requestLogger`, `cors`, `authGuard`, `csp`, `getCspNonce`, `CSP_NONCE_KEY`
1084
+
1085
+ **Sessions:** `createCookieSession`
1086
+
1087
+ **Components:** `Outlet`, `Link`, `Form`, `Scripts`, `LiveReload`, `Await`, `Image`
795
1088
 
796
- **v0.1.21.** All core phases shipped:
1089
+ **Hooks:** `useLoaderData`, `useActionData`, `useParams`, `useNavigation`, `useFetcher`, `useSearchParams`, `useBlocker`, `useLocale`, `useLocalizedLink`
797
1090
 
798
- - File-based routing with trie matcher and layout chains
799
- - Streaming SSR (`renderToReadableStream`) with `defer()` and `<Await>`
800
- - Client hydration, soft navigation, `popstate`, prefetch
801
- - HMR with module-level swap (no full reload)
802
- - Cookie sessions with HMAC-SHA256 and secret rotation
803
- - Middleware pipeline with `requestLogger`, `cors`, `authGuard`
804
- - Production build with content-hashed assets and code splitting
805
- - `<Image>` with on-demand ImageMagick optimization and LRU cache
806
- - Typed routes codegen (`AppRoutes`, `RouteParams<T>`, `TypedLoaderArgs<T>`, `routes` builder)
807
- - `"use server"` / `"use client"` directive system with `/_action` endpoint
808
- - **Programmatic API** — `createDevServer`, `runBuild`, `loadUserConfig` importable without the CLI
809
- - **Native `bun build --compile` support** — module-registry codegen produces single-binary deployables; build plugins exported from the public API; action IDs use relative paths for cross-machine stability
1091
+ **i18n:** `wrapRoutesWithLocale`, `stripLocale`, `localizedDataPath`
810
1092
 
811
- Remaining on the roadmap: streaming `useFetcher()` (full implementation).
1093
+ **Client RPC:** `createClient`
1094
+
1095
+ **Build / programmatic:** `createDevServer`, `runBuild`, `loadUserConfig`
1096
+
1097
+ **Codegen:** `writeModuleRegistries`, `writeManifestModule`, `generateRouteRegistry`, `generateActionRegistry`, `generateManifestModule`
1098
+
1099
+ **Build plugins:** `useClientStubPlugin`, `createUseServerProxyPlugin`, `useServerProxyPlugin`, `serverModuleStubPlugin`, `serverOnlyPlugin`, `clientEnvPlugin`, `cssModulesPlugin`, `transformCssModule`
1100
+
1101
+ **Adapters:** `createCloudflareAdapter`, `makeCloudflareHandler`
1102
+
1103
+ **Types:** `LoaderArgs`, `ActionArgs`, `MetaArgs`, `MetaDescriptor`, `LoaderFunction`, `ActionFunction`, `MetaFunction`, `RouteModule`, `RouteDefinition`, `RouteFile`, `Segment`, `BractJSConfig`, `RenderOptions`, `ServerManifest`, `ContextFactory`, `ApiRouteDefinition`, `AppApiRoutes`, `FieldErrors`, `ValidationError`, `BractAdapter`, `LifecycleHooks`, `MiddlewareFn`, `MiddlewareContext`, `CorsOptions`, `AuthGuardOptions`, `CspOptions`, `SessionStorageLike`, `SessionLike`, `Session`, `SessionStorage`, `SessionData`, `CookieSessionOptions`, `CommitOptions`, `ImageProps`, `ImageFormat`, `ImageFit`, `SearchParamsResult`, `I18nConfig`, `DevServerOptions`, `DevServer`, `BuildConfig`, `CodegenResult`, `ModuleRegistry`, `BractJSContextValue`, `RouteManifest`
812
1104
 
813
1105
  ---
814
1106
 
1107
+ ## Changelog
1108
+
1109
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
1110
+
1111
+ ---
1112
+
1113
+ ## Why BractJS
1114
+
1115
+ - **Bun-native** — `Bun.serve`, `Bun.build`, `Bun.file`, `Bun.Glob`, `Bun.watch`. No Node.js.
1116
+ - **Zero framework deps** — only peers are `react` and `react-dom`.
1117
+ - **Streaming SSR** — `renderToReadableStream()` with `defer()` and `<Await>`.
1118
+ - **File-based routing** — drop a file in `app/routes/`.
1119
+ - **Full-stack** — loaders, actions, sessions, server actions, typed API routes, middleware.
1120
+ - **Typed routes** — codegen wires `<Link>`, `useNavigate`, and `useParams` to your routes (autocompleted paths, typed params), plus a type-safe URL builder.
1121
+ - **Single-binary** — `bun build --compile` to one executable.
1122
+
815
1123
  ## License
816
1124
 
817
1125
  MIT