@bractjs/bractjs 0.1.0

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +586 -0
  3. package/bin/cli.ts +101 -0
  4. package/package.json +58 -0
  5. package/src/__tests__/fixtures/app/root.tsx +9 -0
  6. package/src/__tests__/fixtures/app/routes/_index.tsx +20 -0
  7. package/src/__tests__/integration.test.ts +66 -0
  8. package/src/__tests__/loader.test.ts +89 -0
  9. package/src/__tests__/matcher.test.ts +69 -0
  10. package/src/__tests__/meta.test.ts +81 -0
  11. package/src/__tests__/scanner.test.ts +58 -0
  12. package/src/__tests__/session.test.ts +103 -0
  13. package/src/build/bundler.ts +75 -0
  14. package/src/build/defines.ts +16 -0
  15. package/src/build/directives.ts +67 -0
  16. package/src/build/env-plugin.ts +56 -0
  17. package/src/build/hash.ts +56 -0
  18. package/src/build/manifest.ts +60 -0
  19. package/src/client/ClientRouter.tsx +122 -0
  20. package/src/client/components/Await.tsx +26 -0
  21. package/src/client/components/Form.tsx +67 -0
  22. package/src/client/components/Image.tsx +79 -0
  23. package/src/client/components/Link.tsx +42 -0
  24. package/src/client/components/LiveReload.tsx +16 -0
  25. package/src/client/components/Outlet.tsx +64 -0
  26. package/src/client/components/Scripts.tsx +12 -0
  27. package/src/client/entry.tsx +49 -0
  28. package/src/client/form-utils.ts +12 -0
  29. package/src/client/hooks/useActionData.ts +14 -0
  30. package/src/client/hooks/useFetcher.ts +51 -0
  31. package/src/client/hooks/useLoaderData.ts +14 -0
  32. package/src/client/hooks/useNavigation.ts +12 -0
  33. package/src/client/hooks/useParams.ts +14 -0
  34. package/src/client/nav-utils.ts +35 -0
  35. package/src/client/prefetch.ts +32 -0
  36. package/src/client/route-cache.ts +20 -0
  37. package/src/client/router.tsx +54 -0
  38. package/src/client/types.ts +23 -0
  39. package/src/codegen/route-codegen.ts +99 -0
  40. package/src/dev/error-overlay.ts +33 -0
  41. package/src/dev/hmr-client.ts +43 -0
  42. package/src/dev/hmr-module-handler.ts +47 -0
  43. package/src/dev/hmr-server.ts +51 -0
  44. package/src/dev/rebuilder.ts +95 -0
  45. package/src/dev/server.ts +38 -0
  46. package/src/dev/watcher.ts +32 -0
  47. package/src/image/cache.ts +75 -0
  48. package/src/image/handler.ts +82 -0
  49. package/src/image/optimizer.ts +76 -0
  50. package/src/image/types.ts +27 -0
  51. package/src/index.ts +51 -0
  52. package/src/middleware/authGuard.ts +37 -0
  53. package/src/middleware/cors.ts +36 -0
  54. package/src/middleware/requestLogger.ts +15 -0
  55. package/src/server/action-handler.ts +35 -0
  56. package/src/server/action-registry.ts +41 -0
  57. package/src/server/env.ts +29 -0
  58. package/src/server/index.ts +8 -0
  59. package/src/server/layout.ts +92 -0
  60. package/src/server/loader.ts +80 -0
  61. package/src/server/matcher.ts +99 -0
  62. package/src/server/meta.ts +92 -0
  63. package/src/server/middleware.ts +47 -0
  64. package/src/server/render.ts +65 -0
  65. package/src/server/request-handler.ts +142 -0
  66. package/src/server/response.ts +17 -0
  67. package/src/server/scanner.ts +68 -0
  68. package/src/server/serve.ts +131 -0
  69. package/src/server/session.ts +111 -0
  70. package/src/server/static.ts +40 -0
  71. package/src/shared/context.ts +37 -0
  72. package/src/shared/deferred.ts +55 -0
  73. package/src/shared/error-boundary.tsx +58 -0
  74. package/src/shared/errors.ts +49 -0
  75. package/src/shared/route-types.ts +51 -0
  76. package/templates/new-app/app/root.tsx +20 -0
  77. package/templates/new-app/app/routes/_index.tsx +31 -0
  78. package/templates/new-app/app/routes/about.tsx +21 -0
  79. package/templates/new-app/bractjs.config.ts +14 -0
  80. package/templates/new-app/package.json +20 -0
  81. package/types/config.d.ts +25 -0
  82. package/types/index.d.ts +109 -0
  83. package/types/middleware.d.ts +19 -0
  84. package/types/route.d.ts +41 -0
  85. package/types/session.d.ts +32 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YOUR_NAME
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,586 @@
1
+ # BractJS
2
+
3
+ > Production-grade SSR framework for Bun + React 19.
4
+ > File-based routing · Parallel loaders · Streaming SSR · Built-in HMR · Server Actions
5
+
6
+ ---
7
+
8
+ ## Quick Start
9
+
10
+ ```sh
11
+ # Requires Bun — https://bun.sh
12
+ bunx bractjs new my-app
13
+ cd my-app
14
+ bun run dev
15
+ # → http://localhost:3000
16
+ ```
17
+
18
+ > **From source (pre-publish):** clone the repo, then `bun run bin/cli.ts new my-app`.
19
+
20
+ ---
21
+
22
+ ## File-Based Routing
23
+
24
+ Place files inside `app/routes/`. BractJS scans them at startup.
25
+
26
+ | File | URL |
27
+ |------|-----|
28
+ | `routes/_index.tsx` | `/` |
29
+ | `routes/about.tsx` | `/about` |
30
+ | `routes/blog/_index.tsx` | `/blog` |
31
+ | `routes/blog/[id].tsx` | `/blog/:id` |
32
+ | `routes/docs/[...slug].tsx` | `/docs/*` (catch-all) |
33
+ | `routes/blog/layout.tsx` | wraps all `/blog/*` routes |
34
+
35
+ `app/root.tsx` is the outermost layout (always rendered). Match priority per segment: **static > dynamic > catch-all**.
36
+
37
+ ---
38
+
39
+ ## Route Module API
40
+
41
+ Every file in `app/routes/` can export any combination of these:
42
+
43
+ ```tsx
44
+ import type { LoaderArgs, ActionArgs, MetaArgs } from "bractjs";
45
+ import { redirect } from "bractjs";
46
+
47
+ // Runs on every GET — return value becomes useLoaderData()
48
+ export async function loader({ request, params, context }: LoaderArgs) {
49
+ const post = await db.post.findById(params.id);
50
+ if (!post) throw new Response("Not Found", { status: 404 });
51
+ return { post };
52
+ }
53
+
54
+ export type LoaderData = Awaited<ReturnType<typeof loader>>;
55
+
56
+ // Runs on POST / PUT / DELETE
57
+ export async function action({ params, formData }: ActionArgs) {
58
+ await db.post.update(params.id, { title: formData.get("title") as string });
59
+ return redirect("/blog");
60
+ }
61
+
62
+ // SSR <title> and <meta> tags
63
+ export function meta({ loaderData }: MetaArgs<LoaderData>) {
64
+ return [
65
+ { title: loaderData.post.title },
66
+ { name: "description", content: loaderData.post.excerpt },
67
+ ];
68
+ }
69
+
70
+ // Error boundary for this route segment
71
+ export function ErrorBoundary({ error }: { error: Error }) {
72
+ return <p>Error: {error.message}</p>;
73
+ }
74
+
75
+ // The page component (required)
76
+ export default function BlogPost() {
77
+ const { post } = useLoaderData<LoaderData>();
78
+ return <article><h1>{post.title}</h1></article>;
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Root Layout (`app/root.tsx`)
85
+
86
+ Required. Provides the `<html>` document shell.
87
+
88
+ ```tsx
89
+ import { Scripts, LiveReload, Outlet } from "bractjs";
90
+
91
+ export function meta() {
92
+ return [{ title: "My App" }, { name: "viewport", content: "width=device-width, initial-scale=1" }];
93
+ }
94
+
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
+ );
106
+ }
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Deferred / Streaming Data
112
+
113
+ `defer()` streams slow data without blocking the initial HTML response.
114
+
115
+ ```tsx
116
+ import { defer } from "bractjs";
117
+ import { Await } from "bractjs";
118
+ import { Suspense } from "react";
119
+
120
+ export async function loader({ params }: LoaderArgs) {
121
+ return defer({
122
+ post: await db.post.findById(params.id), // awaited — in initial HTML
123
+ comments: db.comments.forPost(params.id), // Promise — streamed later
124
+ });
125
+ }
126
+
127
+ export default function BlogPost() {
128
+ const { post, comments } = useLoaderData<LoaderData>();
129
+ return (
130
+ <article>
131
+ <h1>{post.title}</h1>
132
+ <Suspense fallback={<p>Loading comments…</p>}>
133
+ <Await resolve={comments}>
134
+ {(c) => <CommentList comments={c} />}
135
+ </Await>
136
+ </Suspense>
137
+ </article>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Client Primitives
145
+
146
+ ### `<Link>`
147
+
148
+ Soft-navigates without a full reload. `prefetch="hover"` preloads the route chunk + loader data on mouse-enter.
149
+
150
+ ```tsx
151
+ import { Link } from "bractjs";
152
+
153
+ <Link to="/blog/42">Read Post</Link>
154
+ <Link to="/about" prefetch="hover">About</Link>
155
+ ```
156
+
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";
163
+
164
+ <Form method="post" action="/blog/new">
165
+ <input name="title" />
166
+ <button type="submit">Create</button>
167
+ </Form>
168
+ ```
169
+
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
+ }
183
+ ```
184
+
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";
199
+
200
+ const { post } = useLoaderData<LoaderData>();
201
+
202
+ const { state } = useNavigation();
203
+ if (state === "loading") return <Spinner />;
204
+
205
+ const fetcher = useFetcher();
206
+ fetcher.load("/api/suggestions?q=bun");
207
+ ```
208
+
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";
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
+ />
234
+ ```
235
+
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
261
+ ```
262
+
263
+ **Generated file provides:**
264
+
265
+ ```ts
266
+ // Every URL pattern as a string literal union
267
+ export type AppRoutes = "/" | "/blog/:id" | "/org/:orgId/repo/:repoId";
268
+
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>;
274
+
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 };
278
+
279
+ // Type-safe route builder
280
+ export const routes = {
281
+ "/": () => "/",
282
+ "/blog/:id": (params: { id: string }) => `/blog/${params.id}`,
283
+ } as const;
284
+ ```
285
+
286
+ **Usage:**
287
+
288
+ ```ts
289
+ // Typed loader — params.id is string, not string | undefined
290
+ import type { TypedLoaderArgs, RouteParams } from "../route-types.gen.ts";
291
+
292
+ export async function loader({ params }: TypedLoaderArgs<"/blog/:id">) {
293
+ return db.post.findById(params.id); // ✓ typed
294
+ }
295
+
296
+ // Typed params hook
297
+ const { id } = useParams<RouteParams<"/blog/:id">>();
298
+
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
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Server Actions & Client Components
308
+
309
+ ### `"use server"` — Server Actions
310
+
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.
312
+
313
+ ```ts
314
+ // app/actions.ts
315
+ "use server";
316
+
317
+ export async function createPost(formData: FormData) {
318
+ const title = formData.get("title") as string;
319
+ await db.insert(posts).values({ title });
320
+ return { ok: true };
321
+ }
322
+
323
+ export async function deletePost(id: string) {
324
+ await db.delete(posts).where(eq(posts.id, id));
325
+ }
326
+ ```
327
+
328
+ ```tsx
329
+ // app/routes/new.tsx — import used normally; on client it becomes a fetch proxy
330
+ import { createPost } from "../actions.ts";
331
+
332
+ export default function NewPost() {
333
+ return (
334
+ <form action={createPost}>
335
+ <input name="title" />
336
+ <button type="submit">Create</button>
337
+ </form>
338
+ );
339
+ }
340
+ ```
341
+
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.
343
+
344
+ ### `"use client"` — Client-Only Components
345
+
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.
347
+
348
+ ```tsx
349
+ // app/components/Counter.tsx
350
+ "use client";
351
+ import { useState } from "react";
352
+
353
+ export function Counter() {
354
+ const [n, setN] = useState(0);
355
+ return <button onClick={() => setN(n + 1)}>Count: {n}</button>;
356
+ }
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Middleware
362
+
363
+ Middleware runs before routing. Register on the module-level `pipeline` singleton.
364
+
365
+ ```ts
366
+ import { pipeline, requestLogger, cors, authGuard } from "bractjs";
367
+
368
+ pipeline
369
+ .use(requestLogger())
370
+ .use(cors({ origin: "https://myapp.com" }))
371
+ .use(authGuard({ session }));
372
+ ```
373
+
374
+ | Middleware | Description |
375
+ |---|---|
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 |
379
+
380
+ **Custom middleware:**
381
+
382
+ ```ts
383
+ import type { MiddlewareFn } from "bractjs";
384
+
385
+ const trace: MiddlewareFn = async (ctx, next) => {
386
+ ctx.context.requestId = crypto.randomUUID();
387
+ return next();
388
+ };
389
+ ```
390
+
391
+ `ctx.context` is threaded into every `loader` and `action` as the `context` argument.
392
+
393
+ ---
394
+
395
+ ## Sessions
396
+
397
+ ```ts
398
+ import { createCookieSession } from "bractjs";
399
+
400
+ const session = createCookieSession({
401
+ 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",
406
+ });
407
+
408
+ export async function loader({ request }: LoaderArgs) {
409
+ const s = await session.getSession(request.headers.get("Cookie"));
410
+ return { user: s.get("user") };
411
+ }
412
+
413
+ export async function action({ request }: ActionArgs) {
414
+ const s = await session.getSession(request.headers.get("Cookie"));
415
+ s.set("user", { id: 1, name: "Alice" });
416
+ return redirect("/dashboard", {
417
+ headers: { "Set-Cookie": await session.commitSession(s) },
418
+ });
419
+ }
420
+ ```
421
+
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`
425
+
426
+ ---
427
+
428
+ ## Environment Variables
429
+
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
+ ## Configuration Reference
444
+
445
+ All fields are optional. BractJS works with zero configuration.
446
+
447
+ | Field | Type | Default | Description |
448
+ |-------|------|---------|-------------|
449
+ | `port` | `number` | `3000` | TCP port |
450
+ | `appDir` | `string` | `"./app"` | Directory containing `routes/` and `root.tsx` |
451
+ | `publicDir` | `string` | `"./public"` | Static assets (served with no-cache) |
452
+ | `buildDir` | `string` | `"./build"` | Output for `bractjs build` |
453
+ | `imageCacheDir` | `string` | `".bract-image-cache"` | Disk cache for optimized images |
454
+ | `sourcemap` | `string` | `"external"` | `"none"` \| `"inline"` \| `"external"` |
455
+ | `minify` | `boolean` | `true` | Minify client bundles |
456
+ | `clientEnv` | `string[]` | `[]` | `process.env` keys exposed to the client |
457
+
458
+ ---
459
+
460
+ ## CLI
461
+
462
+ | Command | Description |
463
+ |---------|-------------|
464
+ | `bractjs new <name>` | Scaffold a new app into `<name>/` |
465
+ | `bractjs dev` | Start dev server with HMR on port 3000 |
466
+ | `bractjs build` | Dual server + client build with content-hashed output |
467
+ | `bractjs start` | Serve the production build |
468
+ | `bractjs codegen [app] [out]` | Generate typed route types into `app/route-types.gen.ts` |
469
+
470
+ ---
471
+
472
+ ## App Directory Structure
473
+
474
+ ```
475
+ my-app/
476
+ ├── app/
477
+ │ ├── root.tsx # required — <html> shell
478
+ │ ├── route-types.gen.ts # generated by bractjs codegen
479
+ │ ├── actions.ts # "use server" actions
480
+ │ └── routes/
481
+ │ ├── _index.tsx # → /
482
+ │ ├── about.tsx # → /about
483
+ │ ├── blog/
484
+ │ │ ├── layout.tsx # layout for /blog/*
485
+ │ │ ├── _index.tsx # → /blog
486
+ │ │ └── [id].tsx # → /blog/:id
487
+ │ └── docs/
488
+ │ └── [...slug].tsx # → /docs/*
489
+ ├── public/
490
+ │ └── favicon.ico
491
+ └── build/ # generated — do not edit
492
+ ├── server/
493
+ ├── client/
494
+ └── route-manifest.json
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Architecture
500
+
501
+ ```
502
+ Request
503
+ └─ Middleware pipeline
504
+ └─ /_action → Server Action registry → fn(...args)
505
+ └─ /_image → ImageMagick transform → LRU cache → Response
506
+ └─ Route trie (static > param > catch-all)
507
+ └─ Layout chain (root → layout → route)
508
+ └─ Parallel loaders (Promise.all)
509
+ └─ renderToReadableStream → streaming Response
510
+ ```
511
+
512
+ Client:
513
+ ```
514
+ hydrateRoot(document)
515
+ └─ ClientRouter (RouterContext + NavigationContext)
516
+ └─ Outlet → React.lazy route chunk
517
+ └─ useLoaderData / useParams / useNavigation / …
518
+ ```
519
+
520
+ Build pipeline (`bractjs build`):
521
+ ```
522
+ 1. codegen → app/route-types.gen.ts
523
+ 2. server bundle → Bun.build (target: bun) + useClientStubPlugin
524
+ 3. client bundle → Bun.build (target: browser, splitting) + useServerProxyPlugin
525
+ 4. content-hash → rename outputs, write route-manifest.json
526
+ ```
527
+
528
+ ---
529
+
530
+ ## Package Structure
531
+
532
+ ```
533
+ bractjs/
534
+ ├── src/
535
+ │ ├── server/ # SSR, routing, loaders, actions, sessions, action-registry
536
+ │ ├── client/ # hydrateRoot, contexts, hooks, Link/Form/Image components
537
+ │ ├── build/ # Bun.build orchestration, manifest, hashing, directives
538
+ │ ├── codegen/ # route-types.gen.ts generator
539
+ │ ├── image/ # /_image handler, ImageMagick optimizer, LRU cache
540
+ │ ├── dev/ # watcher, HMR server + client, error overlay
541
+ │ ├── shared/ # types, errors, deferred, context
542
+ │ └── middleware/ # requestLogger, cors, authGuard
543
+ ├── bin/cli.ts
544
+ ├── types/ # TypeScript declaration files
545
+ └── templates/
546
+ └── new-app/ # scaffold template
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Why BractJS
552
+
553
+ - **Bun-native** — `Bun.serve`, `Bun.build`, `Bun.file`, `Bun.Glob`, `Bun.watch`. No Node.js.
554
+ - **Zero framework deps** — only peer dependencies are `react` and `react-dom`.
555
+ - **Streaming SSR** — `renderToReadableStream()` with `defer()` for slow data.
556
+ - **File-based routing** — drop a file in `app/routes/`, it's a route.
557
+ - **Full-stack** — loaders, actions, sessions, server actions, and middleware in one package.
558
+ - **Typed routes** — codegen produces per-route param types and a type-safe route builder.
559
+
560
+ ---
561
+
562
+ ## Status
563
+
564
+ **v0.1.0 complete.** All core phases shipped:
565
+
566
+ - File-based routing with trie matcher and layout chains
567
+ - Streaming SSR (`renderToReadableStream`) with `defer()` and `<Await>`
568
+ - Client hydration, soft navigation, `popstate`, prefetch
569
+ - HMR with module-level swap (no full reload)
570
+ - Cookie sessions with HMAC-SHA256 and secret rotation
571
+ - Middleware pipeline with `requestLogger`, `cors`, `authGuard`
572
+ - Production build with content-hashed assets and code splitting
573
+
574
+ Post-v0.1.0 features also shipped:
575
+
576
+ - `<Image>` with on-demand ImageMagick optimization and LRU cache
577
+ - Typed routes codegen (`AppRoutes`, `RouteParams<T>`, `TypedLoaderArgs<T>`, `routes` builder)
578
+ - `"use server"` / `"use client"` directive system with `/_action` endpoint
579
+
580
+ Remaining on the roadmap: Edge runtime (Cloudflare Workers), CSS modules, i18n routing, streaming `useFetcher()`.
581
+
582
+ ---
583
+
584
+ ## License
585
+
586
+ MIT
package/bin/cli.ts ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bun
2
+ import { join, resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ export {}; // make this file a module
5
+
6
+ const command = process.argv[2];
7
+
8
+ // ── new <app-name> ──────────────────────────────────────────────────────────
9
+
10
+ async function scaffoldNew(appName: string): Promise<void> {
11
+ if (!appName) {
12
+ console.error("Usage: bractjs new <app-name>");
13
+ process.exit(1);
14
+ }
15
+
16
+ const appDir = resolve(process.cwd(), appName);
17
+ if (existsSync(appDir)) {
18
+ console.error(`Directory "${appName}" already exists.`);
19
+ process.exit(1);
20
+ }
21
+
22
+ const templateDir = join(import.meta.dirname, "../templates/new-app");
23
+ // Absolute path to the bractjs package itself — used as a file: dep before npm publish
24
+ const bractPackageDir = resolve(import.meta.dirname, "..");
25
+ console.log(`Creating ${appName}...`);
26
+
27
+ // Recursively copy template files, substituting {{APP_NAME}} and {{BRACT_PATH}}
28
+ await copyDir(templateDir, appDir, appName, bractPackageDir);
29
+
30
+ // Install dependencies
31
+ console.log("Installing dependencies...");
32
+ const result = Bun.spawnSync(["bun", "install"], { cwd: appDir, stdio: ["inherit", "inherit", "inherit"] });
33
+ if (result.exitCode !== 0) {
34
+ console.error("bun install failed.");
35
+ process.exit(result.exitCode ?? 1);
36
+ }
37
+
38
+ console.log(`\n✓ Created ${appName}\n`);
39
+ console.log("Next steps:");
40
+ console.log(` cd ${appName}`);
41
+ console.log(" bun run dev");
42
+ }
43
+
44
+ async function copyDir(src: string, dest: string, appName: string, bractPath: string): Promise<void> {
45
+ const glob = new Bun.Glob("**/*");
46
+ for await (const rel of glob.scan({ cwd: src, onlyFiles: true })) {
47
+ const srcPath = join(src, rel);
48
+ const destPath = join(dest, rel);
49
+ // Ensure parent directory exists
50
+ const parentDir = destPath.slice(0, destPath.lastIndexOf("/"));
51
+ await Bun.write(destPath, ""); // creates parent dirs
52
+ let content = await Bun.file(srcPath).text();
53
+ content = content.replaceAll("{{APP_NAME}}", appName);
54
+ content = content.replaceAll("{{BRACT_PATH}}", bractPath);
55
+ await Bun.write(destPath, content);
56
+ }
57
+ }
58
+
59
+ // ── dispatch ────────────────────────────────────────────────────────────────
60
+
61
+ switch (command) {
62
+ case "new":
63
+ await scaffoldNew(process.argv[3]);
64
+ break;
65
+
66
+ case "dev":
67
+ await import("../src/dev/server.ts");
68
+ break;
69
+
70
+ case "build": {
71
+ const { runBuild } = await import("../src/build/bundler.ts");
72
+ await runBuild({ port: 3000, appDir: "./app", publicDir: "./public", buildDir: "./build", manifest: { clientEntry: "", routes: {} } });
73
+ break;
74
+ }
75
+
76
+ case "start": {
77
+ const { createServer } = await import("../src/server/serve.ts");
78
+ createServer({ port: 3000, buildDir: "./build" });
79
+ break;
80
+ }
81
+
82
+ case "codegen": {
83
+ const { writeRouteTypes } = await import("../src/codegen/route-codegen.ts");
84
+ const appDir = resolve(process.cwd(), process.argv[3] ?? "./app");
85
+ const outPath = process.argv[4] ? resolve(process.cwd(), process.argv[4]) : undefined;
86
+ await writeRouteTypes(appDir, outPath);
87
+ break;
88
+ }
89
+
90
+ default:
91
+ console.log(
92
+ "Usage: bractjs <command>\n" +
93
+ " new <app-name> Scaffold a new BractJS app\n" +
94
+ " dev Start dev server with HMR\n" +
95
+ " build Build for production\n" +
96
+ " start Start production server\n" +
97
+ " codegen [app] [out] Generate typed route types",
98
+ );
99
+ process.exit(1);
100
+ }
101
+