brustjs 0.1.50-alpha → 0.1.52-alpha

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 (90) hide show
  1. package/package.json +39 -15
  2. package/runtime/cache-sync.ts +291 -0
  3. package/runtime/cache.ts +4 -0
  4. package/runtime/cli/dev.ts +7 -0
  5. package/runtime/cli/native-routes-emit.ts +147 -1
  6. package/runtime/config.ts +42 -0
  7. package/runtime/index.d.ts +63 -0
  8. package/runtime/index.js +57 -52
  9. package/runtime/index.ts +108 -9
  10. package/runtime/native/runtime.ts +220 -7
  11. package/runtime/render/fragment.ts +87 -0
  12. package/runtime/routes.ts +225 -48
  13. package/runtime/templates.ts +47 -0
  14. package/runtime/treaty.ts +24 -1
  15. package/types/action-error.d.ts +18 -0
  16. package/types/cache-sync.d.ts +42 -0
  17. package/types/cache.d.ts +20 -0
  18. package/types/cli/help.d.ts +28 -0
  19. package/types/cli/jinja-staleness.d.ts +14 -0
  20. package/types/cli/native-routes-emit.d.ts +217 -0
  21. package/types/cli/new.d.ts +30 -0
  22. package/types/cli/templates.d.ts +39 -0
  23. package/types/client/index.d.ts +14 -0
  24. package/types/config.d.ts +42 -0
  25. package/types/cookies.d.ts +25 -0
  26. package/types/create.d.ts +1 -0
  27. package/types/css/build.d.ts +11 -0
  28. package/types/css/component-build.d.ts +17 -0
  29. package/types/css/component-loader.d.ts +8 -0
  30. package/types/css/manifest.d.ts +21 -0
  31. package/types/css/process-modules.d.ts +31 -0
  32. package/types/css/route-deps.d.ts +20 -0
  33. package/types/css/scan-imports.d.ts +13 -0
  34. package/types/css.d.ts +16 -0
  35. package/types/define-actions.d.ts +133 -0
  36. package/types/dev/client.d.ts +8 -0
  37. package/types/dev/coordinator.d.ts +33 -0
  38. package/types/dev/inject.d.ts +6 -0
  39. package/types/dev/jinja-reload.d.ts +7 -0
  40. package/types/dev/tui.d.ts +35 -0
  41. package/types/dev/watcher.d.ts +34 -0
  42. package/types/dev/worker-registry.d.ts +17 -0
  43. package/types/dev/ws-channel.d.ts +39 -0
  44. package/types/generator.d.ts +23 -0
  45. package/types/index.d.ts +222 -0
  46. package/types/islands/brust-page.d.ts +74 -0
  47. package/types/islands/build.d.ts +49 -0
  48. package/types/islands/chunk-id.d.ts +10 -0
  49. package/types/islands/importmap.d.ts +2 -0
  50. package/types/islands/island.d.ts +65 -0
  51. package/types/islands/isr-jsx.d.ts +31 -0
  52. package/types/islands/native-render.d.ts +89 -0
  53. package/types/loader-cache.d.ts +18 -0
  54. package/types/mcp/extractor.d.ts +14 -0
  55. package/types/mcp/manifest.d.ts +23 -0
  56. package/types/mcp/schema.d.ts +19 -0
  57. package/types/mcp/server.d.ts +15 -0
  58. package/types/md/emit.d.ts +72 -0
  59. package/types/md/render.d.ts +80 -0
  60. package/types/md/routes.d.ts +119 -0
  61. package/types/md/scan.d.ts +34 -0
  62. package/types/md/slug.d.ts +1 -0
  63. package/types/native/build.d.ts +30 -0
  64. package/types/native/index.d.ts +2 -0
  65. package/types/native/runtime.d.ts +52 -0
  66. package/types/navigation/active-nav.d.ts +2 -0
  67. package/types/navigation/index.d.ts +5 -0
  68. package/types/navigation/navigate.d.ts +14 -0
  69. package/types/navigation/react.d.ts +15 -0
  70. package/types/navigation/store.d.ts +44 -0
  71. package/types/render/fragment.d.ts +20 -0
  72. package/types/render/inject-action-prefix.d.ts +9 -0
  73. package/types/render/inject-css-link.d.ts +8 -0
  74. package/types/render/inject-dev-client.d.ts +6 -0
  75. package/types/render/inject-generator.d.ts +7 -0
  76. package/types/render/inject-store.d.ts +9 -0
  77. package/types/render/stream.d.ts +45 -0
  78. package/types/request-context.d.ts +16 -0
  79. package/types/routes.d.ts +506 -0
  80. package/types/sse/handler.d.ts +22 -0
  81. package/types/standard-schema.d.ts +31 -0
  82. package/types/store/define-store.d.ts +31 -0
  83. package/types/store/index.d.ts +5 -0
  84. package/types/store/react.d.ts +2 -0
  85. package/types/store/serialize.d.ts +5 -0
  86. package/types/store/server-context.d.ts +4 -0
  87. package/types/store/signal.d.ts +18 -0
  88. package/types/templates.d.ts +18 -0
  89. package/types/treaty.d.ts +70 -0
  90. package/types/ws/handler.d.ts +26 -0
@@ -0,0 +1,506 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ import type { EndpointDef } from './define-actions.ts';
3
+ export { mdNav, mdRoutes, mdUrlPath } from './md/routes.ts';
4
+ export type { MdNavGroup, MdNavItem, MdRoute, MdRoutesOptions, MdRouteSource, } from './md/routes.ts';
5
+ export { scanMdDir } from './md/scan.ts';
6
+ export type { MdFile } from './md/scan.ts';
7
+ export { slugifyHeading } from './md/slug.ts';
8
+ export declare const NEVER_ABORTS: AbortSignal;
9
+ /** Structured view of the request, parsed once in Rust and shipped in the
10
+ * JSON envelope. Header names are lower-cased. Cookies are parsed from the
11
+ * Cookie header. `search` is the query string parsed as key→value (last
12
+ * occurrence wins on duplicates). */
13
+ export interface BrustRequest {
14
+ method: string;
15
+ /** Full request URL path including query string, e.g. `/foo?bar=1`. */
16
+ url: string;
17
+ headers: Record<string, string>;
18
+ cookies: Record<string, string>;
19
+ search: Record<string, string>;
20
+ /** AbortSignal that fires when the client disconnects mid-request.
21
+ * SSE-only in MVP — non-SSE routes receive NEVER_ABORTS (a permanently-
22
+ * unaborted shared sentinel; signal.aborted === false forever). Real
23
+ * disconnect detection for render/action is a follow-up. */
24
+ signal: AbortSignal;
25
+ /** Matched route params (percent-decoded) — the SAME values the loader ctx
26
+ * receives. Populated by the TS dispatch layer (`prepReq`) BEFORE the
27
+ * middleware chain runs, so middleware can authorize against `{id}` without
28
+ * re-parsing the raw URL. Empty object for routes without params — and for
29
+ * SSE/WS requests (their envelope carries no params in v1). */
30
+ params: Record<string, string>;
31
+ /** Request-scoped bag middleware writes and loaders/handlers read. The same
32
+ * object identity flows through the whole chain (middleware → loader →
33
+ * component/handler). Locals written before `next()` are visible downstream;
34
+ * writes AFTER `next()` returns happen after the loader already ran. Reset
35
+ * to a fresh `{}` per request. */
36
+ locals: Record<string, unknown>;
37
+ }
38
+ /** Populate the TS-owned `BrustRequest` fields (`params`, `locals`) on a
39
+ * request that just arrived in the Rust envelope. Rust knows nothing of these
40
+ * fields, so every dispatch branch calls this ONCE, before composeChain /
41
+ * loaders run — the single population point. Mutates and returns `req`. */
42
+ export declare function prepReq(req: BrustRequest, params: Record<string, string> | undefined): BrustRequest;
43
+ /** Handler module shape — what `() => Promise<WsHandlers>` resolves to.
44
+ * open/message/close are all OPTIONAL — a no-op WebSocket (handshake
45
+ * only, e.g. liveness probe) is a valid use case. */
46
+ export interface WsHandlers {
47
+ /** Called once after the 101 handshake completes. Use this to record
48
+ * the socket in your in-memory map, send a hello frame, etc.
49
+ * Throwing here closes the conn with 1011 (Internal Error); on_close
50
+ * does NOT fire (we never reached steady state). */
51
+ open?: (socket: WsSocket, ctx: {
52
+ req: BrustRequest;
53
+ subprotocol: string | null;
54
+ }) => void | Promise<void>;
55
+ /** Called per incoming message frame. data is string for Text frames,
56
+ * Uint8Array for Binary. Throwing here is logged but the conn stays
57
+ * open — one bad message shouldn't kill the conn; wrap in try/catch
58
+ * for strict-close semantics. */
59
+ message?: (socket: WsSocket, data: string | Uint8Array) => void | Promise<void>;
60
+ /** Called exactly ONCE when the conn closes EXCEPT when the author
61
+ * called socket.close themselves. Code/reason from the RFC 6455
62
+ * close frame; 1006 for abnormal (RST), 1011 for pong timeout,
63
+ * 1001 for server shutdown. */
64
+ close?: (socket: WsSocket, code: number, reason: string) => void;
65
+ }
66
+ /** The only handle the author touches inside a WsHandlers callback. */
67
+ export interface WsSocket {
68
+ /** Send a frame. Text if data is string, Binary if Uint8Array. Returns
69
+ * a Promise that resolves when the TCP write completes (cooperative
70
+ * backpressure, same model as SSE napi.write). Rejects with a clear
71
+ * error if the conn is already closed. */
72
+ send(data: string | Uint8Array): Promise<void>;
73
+ /** Initiate close with optional code (default 1000) and reason (default
74
+ * ''). Idempotent — second call is a no-op. on_close does NOT fire
75
+ * after this call. */
76
+ close(code?: number, reason?: string): void;
77
+ /** Stable per-conn identifier. Useful as a Map key in author's
78
+ * in-memory connection registry. */
79
+ readonly id: bigint;
80
+ }
81
+ export interface RouteContext<Params = Record<string, string>, Data = unknown> {
82
+ params: Params;
83
+ path: string;
84
+ /** Value returned by `route.loader`. Undefined if the route has no loader. */
85
+ data: Data;
86
+ /** Bun Worker id rendering this request. null before the first registerRenderer
87
+ * return resolves (a brief window during boot). */
88
+ workerId: number | null;
89
+ /** Structured request shape. Available to components for read-only inspection. */
90
+ req: BrustRequest;
91
+ }
92
+ export interface ErrorBoundaryProps {
93
+ error: Error;
94
+ }
95
+ /** L2 programmatic cache-key result. The `key` function builds the COMPLETE
96
+ * cache key (you concat url + query + DB-derived data yourself). */
97
+ export interface CacheKeyResult {
98
+ /** The COMPLETE L2 cache key. */
99
+ key: string;
100
+ /** Tag groups for `cache.invalidate({ tags })`. */
101
+ tags?: string[];
102
+ /** Seconds; overrides `key_ttl_seconds` / `ttl_seconds`. */
103
+ ttl?: number;
104
+ }
105
+ export interface RouteCacheConfig<Params = Record<string, string>> {
106
+ /** Base TTL in seconds (L1; L2 fallback). */
107
+ ttl_seconds: number;
108
+ /** L1 declarative key prefix expression (evaluated in Rust, zero-Bun on hit). */
109
+ prefix?: string;
110
+ /** Route to L2 when truthy: a key-expression (conditional) or `true` (always).
111
+ * Absent / `false` ⇒ L1 only. */
112
+ bypass?: string | boolean;
113
+ /** L2 programmatic key (runs in the worker). Returns the COMPLETE key. */
114
+ key?: (ctx: {
115
+ req: BrustRequest;
116
+ url: URL;
117
+ params: Params;
118
+ }) => CacheKeyResult | Promise<CacheKeyResult>;
119
+ /** Static L2 TTL (seconds); `CacheKeyResult.ttl` overrides per-entry. */
120
+ key_ttl_seconds?: number;
121
+ /** Static L1 invalidation tags. L1 entries cached for this route carry these
122
+ * tags so `cache.invalidate({ tags })` evicts them (L1 is no longer
123
+ * TTL-only). Route-level + static — not per-request. */
124
+ tags?: string[];
125
+ }
126
+ /** SSG config — read ONLY by `brust build --ssg`. Live server / dev ignore it.
127
+ * See docs/superpowers/specs/2026-06-12-ssg-dynamic-params-design.md. */
128
+ export interface RouteSsgConfig {
129
+ /** generateStaticParams: concrete param records to prerender. Each record
130
+ * must cover every `{name}` in the route's full path with a non-empty
131
+ * string. Sync or async. Values are URL-encoded into the crawl path. */
132
+ params?: () => Array<Record<string, string>> | Promise<Array<Record<string, string>>>;
133
+ /** What non-prerendered paths do on a static host. 'none' (default) = skip
134
+ * → host 404 (today's behavior). 'client' = client-loader takeover
135
+ * (Phase B; requires `export const clientLoader` in the leaf component
136
+ * file, leaf must be a DEFAULT import in routes.tsx, React routes only). */
137
+ fallback?: 'none' | 'client';
138
+ /** Server-rendered loading UI baked into the fallback shell (Phase B).
139
+ * Renders in the leaf position with NO data. */
140
+ placeholder?: ComponentType;
141
+ }
142
+ /** Shape returned by a middleware or by the terminal `next()` (loader + render).
143
+ * Middleware can short-circuit by returning a RouteResponse without calling next,
144
+ * or call next() and mutate the returned response (status, headers). */
145
+ export interface RouteResponse {
146
+ status: number;
147
+ body: string;
148
+ /** Extra response headers. Names are case-insensitive on the wire; Rust
149
+ * deduplicates by lower-casing internally. Skips collisions with the fixed
150
+ * Content-Type / Content-Length / Connection lines. */
151
+ headers?: Record<string, string>;
152
+ /** Override the Content-Type emitted by Rust. Action returns set this to
153
+ * 'application/json; charset=utf-8'; middleware short-circuits with a
154
+ * raw string body can set 'text/plain'. Falls back to 'text/html' (render)
155
+ * or 'application/json' (action) when omitted. */
156
+ contentType?: string;
157
+ }
158
+ declare const BRUST_VERDICT: unique symbol;
159
+ /** Sentinel returned from a native (`native: true`) route loader to control the
160
+ * HTTP response. Build it with `notFound()` / `redirect()`, never by hand. */
161
+ export interface NativeVerdict {
162
+ readonly [BRUST_VERDICT]: true;
163
+ readonly status: number;
164
+ readonly render: boolean;
165
+ readonly data?: unknown;
166
+ readonly headers?: Record<string, string>;
167
+ }
168
+ /** Signal "not found" from a route loader. ONE helper, two call shapes —
169
+ * matching how each render path consumes a loader result:
170
+ *
171
+ * - NATIVE route loader: `return notFound(data?)`. The returned verdict is
172
+ * inspected by `runNativeChainLoaders` and renders the route's OWN template
173
+ * at HTTP 404 (`data` default `{}` becomes the template context).
174
+ *
175
+ * - REACT route loader: `throw notFound()`. A React loader's RETURN value is
176
+ * the component's `data` prop (never inspected as a verdict), so the only
177
+ * way to signal not-found is to throw. The render dispatch discriminates the
178
+ * thrown verdict (it is symbol-tagged — `isNativeVerdict`) BEFORE the generic
179
+ * 500/errorBoundary handler and renders the NEAREST catch-all (`path: '*'`)
180
+ * for the route's prefix at HTTP 404 — NOT the route's own Component, NOT a
181
+ * 500. If no catch-all is registered for the prefix, a framework default 404
182
+ * body is rendered. `data` is ignored on the React throw path (the catch-all
183
+ * runs its own loader chain).
184
+ *
185
+ * Same value type (`NativeVerdict`) either way — native returns it, React
186
+ * throws it — so there is a single public `notFound()` API. */
187
+ export declare function notFound(data?: unknown): NativeVerdict;
188
+ /** Return from a native route loader to emit a redirect (no template render). */
189
+ export declare function redirect(location: string, status?: 301 | 302 | 303 | 307 | 308): NativeVerdict;
190
+ /** True if a loader return value is a NativeVerdict (symbol-keyed — a plain
191
+ * object with a `status` property is NOT mistaken for one). */
192
+ export declare function isNativeVerdict(x: unknown): x is NativeVerdict;
193
+ declare const HTTP_ERROR: unique symbol;
194
+ export interface HttpErrorOpts {
195
+ /** Override the Content-Type. Defaults: string body → `text/plain;
196
+ * charset=utf-8`, object body → `application/json; charset=utf-8`. */
197
+ contentType?: string;
198
+ /** Extra response headers (e.g. `WWW-Authenticate`). */
199
+ headers?: Record<string, string>;
200
+ }
201
+ /** The symbol-keyed value `httpError()` throws. Body/contentType are resolved
202
+ * at construction so every catch site emits the same wire shape. Mirrors the
203
+ * NativeVerdict / ActionError branding (Symbol.for survives the trigger being
204
+ * duplicated across bundles). */
205
+ export interface HttpErrorTrigger {
206
+ readonly [HTTP_ERROR]: true;
207
+ readonly status: number;
208
+ readonly body: string;
209
+ readonly contentType: string;
210
+ readonly headers?: Record<string, string>;
211
+ }
212
+ /** Throw from a route loader (React or native) to short-circuit the response
213
+ * with an arbitrary error status — the loader-side analogue of a middleware
214
+ * short-circuit:
215
+ *
216
+ * ```ts
217
+ * loader: async ({ req }) => {
218
+ * if (!req.locals.user) throw httpError(403, 'no entry')
219
+ * …
220
+ * }
221
+ * ```
222
+ *
223
+ * THROW-only (it never returns) — one usage form on both render paths, unlike
224
+ * `notFound()` which native loaders RETURN. `body`: string → text/plain (or
225
+ * `opts.contentType`), object → JSON, omitted → empty. Status must be an
226
+ * integer in 400-599: 3xx is `redirect()`'s job, a 404 that renders a page is
227
+ * `notFound()`'s. The response is a plain short-circuit — it never reaches the
228
+ * errorBoundary and never logs as a 500. */
229
+ export declare function httpError(status: number, body?: string | object, opts?: HttpErrorOpts): never;
230
+ /** True when a thrown value is the `httpError()` trigger (symbol-keyed — a
231
+ * plain object with a `status` property is NOT mistaken for one). */
232
+ export declare function isHttpErrorTrigger(x: unknown): x is HttpErrorTrigger;
233
+ /** Loader context passed to native chain loaders. */
234
+ export interface NativeLoaderCtx {
235
+ params: Record<string, string>;
236
+ path: string;
237
+ req: BrustRequest;
238
+ }
239
+ /** Result of running a native route's chain loaders top-down: a merged flat
240
+ * context object (all loader results shallow-merged, child keys win), the
241
+ * first verdict encountered (top-down) which short-circuits the chain, or an
242
+ * `httpError()` trigger THROWN by a loader (intercepted per-loader so it never
243
+ * reaches the caller's generic 500 catch). */
244
+ export type NativeChainResult = {
245
+ data: Record<string, unknown>;
246
+ } | {
247
+ verdict: NativeVerdict;
248
+ } | {
249
+ httpError: HttpErrorTrigger;
250
+ };
251
+ /** Run a native route's `flat.chain` loaders top-down (parent → leaf) and merge
252
+ * their results into ONE flat context object.
253
+ *
254
+ * Semantics (T3 — native <Outlet> loader chain):
255
+ * - Each chain node may lack a loader → skip it.
256
+ * - Results merge shallow: `merged = { ...merged, ...result }` — later (child)
257
+ * keys win over earlier (parent) keys.
258
+ * - First verdict wins: if any loader returns a `notFound()`/`redirect()`
259
+ * verdict (top-down), STOP — remaining loaders are NOT run — and return it.
260
+ * - `chain.length === 1` (or only the leaf has a loader) → behaves exactly as
261
+ * the old leaf-only read (no regression).
262
+ *
263
+ * The caller is responsible for wrapping this in a SINGLE `runInStoreContext`
264
+ * (one Map per request) so parent loader store writes are visible to child
265
+ * loaders — mirroring the React path. This helper itself does NOT open a store
266
+ * scope. */
267
+ export declare function runNativeChainLoaders(chain: Route[], ctx: NativeLoaderCtx): Promise<NativeChainResult>;
268
+ /** Middleware contract — Express/Koa-style chain. Receives a structured
269
+ * request and a `next()` that runs the rest of the chain (eventually the
270
+ * loader + render). Return a `RouteResponse` to short-circuit, or call
271
+ * `await next()` and return its (possibly mutated) result. */
272
+ export type Middleware = (req: BrustRequest, next: () => Promise<RouteResponse>) => Promise<RouteResponse>;
273
+ export interface Route<Params = Record<string, string>, Data = unknown> {
274
+ /** Relative path segment (matchit syntax). Empty string `''` = layout-only
275
+ * (this node contributes nothing to the path; children attach to ancestors).
276
+ * Mutually exclusive with `index: true`. */
277
+ path?: string;
278
+ /** Index route — matches the parent path exactly. Must be a leaf (no
279
+ * `children`, no `path`). Mutually exclusive with `path`. */
280
+ index?: boolean;
281
+ /** TypeScript can't infer per-entry generics inside a `defineRoutes([...])`
282
+ * literal, so the array would force every Component to accept the default
283
+ * `RouteContext<Record<string, string>, unknown>` shape — components that
284
+ * narrow `Params` / `Data` would fail to type-check. Keep the field
285
+ * permissive here; each component self-declares its expected props. */
286
+ Component?: ComponentType<any>;
287
+ /** Optional async function that runs in the worker before rendering. Its
288
+ * return value becomes the component's `data` prop. Exceptions are caught
289
+ * by `errorBoundary` if declared (inherited from closest ancestor). */
290
+ loader?: (ctx: {
291
+ params: Params;
292
+ path: string;
293
+ req: BrustRequest;
294
+ }) => Promise<Data>;
295
+ /** Optional component invoked when Component or loader throws. Inherited
296
+ * by descendants when they don't define their own. */
297
+ errorBoundary?: ComponentType<ErrorBoundaryProps>;
298
+ /** Opt-in cache. Cache config from the leaf only — parent's cache is
299
+ * ignored when the route is reached as part of a chain. */
300
+ cache?: RouteCacheConfig;
301
+ /** Opt-in SSG behavior for dynamic-param routes (`/blog/{slug}`). Only
302
+ * consulted by `brust build --ssg`. */
303
+ ssg?: RouteSsgConfig;
304
+ /** Per-route middleware chain. Runs in declaration order; concatenated
305
+ * with parent middlewares (parent runs before child). Cache lookup
306
+ * still happens BEFORE any middleware (existing rule). */
307
+ middleware?: Middleware[];
308
+ /** Nested children. Each child's path is composed with this node's path
309
+ * via `joinPath` (see flattenRoutes). */
310
+ children?: Route[];
311
+ /** When set, this route streams a text/event-stream response. Cannot
312
+ * coexist with Component, loader, or children (validated at defineRoutes
313
+ * time). The framework auto-sends Content-Type, Cache-Control,
314
+ * X-Accel-Buffering, plus a `: ping\n\n` heartbeat every 15s (opt-out
315
+ * via sseOptions). The author returns ReadableStream<Uint8Array | string>;
316
+ * string chunks are UTF-8 encoded. Listen on req.signal for disconnect. */
317
+ sse?: (req: BrustRequest) => ReadableStream<Uint8Array | string> | Promise<ReadableStream<Uint8Array | string>>;
318
+ sseOptions?: {
319
+ /** Auto-emit `: ping\n\n` every N ms. Default 15000. Set 0 to disable. */
320
+ heartbeatMs?: number;
321
+ };
322
+ /** When set, this route accepts WebSocket upgrades. Cannot coexist
323
+ * with Component, loader, sse, or children (validated at defineRoutes
324
+ * time). The factory is invoked lazily by the WS dispatch path and
325
+ * cached per worker. The handler module may export `WsHandlers`
326
+ * directly OR as a `default` (the common `() => import('./ws-foo')`
327
+ * pattern returns a module namespace `{ default: WsHandlers }`);
328
+ * `handleWsConn` unwraps `.default` defensively at call time. */
329
+ websocket?: () => Promise<WsHandlers | {
330
+ default: WsHandlers;
331
+ }>;
332
+ wsOptions?: {
333
+ /** Server-initiated ping interval in ms. Default 30000. Set 0 to disable.
334
+ * Pong timeout = 2× pingMs; conn closes with code 1011 if no pong by then. */
335
+ pingMs?: number;
336
+ /** Max message size in bytes. Default 1 048 576 (1 MB). Larger frames
337
+ * close the conn with 1009 (Message Too Big). */
338
+ maxMessageBytes?: number;
339
+ /** Subprotocols the route supports (Sec-WebSocket-Protocol). Client's
340
+ * requested list is intersected with this; first match (in this declared
341
+ * order) wins and is reflected in Sec-WebSocket-Protocol response. */
342
+ subprotocols?: string[];
343
+ };
344
+ /** Sub-project J — render this route via the native (jinja) engine, not React.
345
+ * `Component` is REQUIRED (the JSX file is the source jsx-rustc compiles
346
+ * into `.brust/jinja/<Component.name>.jinja`). Loader-friendly: the loader's
347
+ * return value becomes the template context. */
348
+ native?: boolean;
349
+ }
350
+ /** Internal post-flatten representation. Each FlatRoute is a single leaf or
351
+ * index route in the user's nested tree. Indexed by Rust's route_id (array
352
+ * position). Consumed by `makeRenderer` and structurally compatible with
353
+ * `brust.registerRoutes` (reads only `fullPath` and `cache`). */
354
+ export interface FlatRoute {
355
+ /** Full path Rust matches against. Composed from the chain via joinPath. */
356
+ fullPath: string;
357
+ /** Chain of Route nodes from root to leaf, inclusive. Renderer walks
358
+ * this top-down. */
359
+ chain: Route[];
360
+ /** Concatenated middleware from root → leaf. composeChain wraps right-to-left,
361
+ * so the first element here becomes the outermost wrap (runs first). */
362
+ middleware: Middleware[];
363
+ /** Closest errorBoundary in the chain (leaf wins; falls back up the chain). */
364
+ errorBoundary?: ComponentType<ErrorBoundaryProps>;
365
+ /** Cache from the leaf only — no parent inheritance. */
366
+ cache?: RouteCacheConfig;
367
+ /** Sub-project J — Component.name when leaf had `native: true`. Captured
368
+ * at flatten time (build-time AST identifier), so minifier-safe. */
369
+ nativeTemplate?: string;
370
+ /** Catch-all (`{ path: '*' }`) marker. When true this FlatRoute is a
371
+ * "not found" fallback: it stays in the array at its natural index (route_id
372
+ * stable) but install SKIPS the matchit insert for it — it only renders when
373
+ * matchit returns NoMatch under `notFoundPrefix`. NEVER remove a flagged
374
+ * entry from the flat array (that would shift every later route_id).
375
+ *
376
+ * A catch-all render is stamped HTTP 404 UNCONDITIONALLY (spec invariant 4:
377
+ * "never 200"). A `redirect()` from the catch-all's OWN loader still wins —
378
+ * redirect verdicts short-circuit and return before the 404 stamp on every
379
+ * path. A non-redirect verdict status, however, is overridden by 404 (a 404
380
+ * page is a 404, by definition). */
381
+ notFound?: boolean;
382
+ /** Parent layout's path prefix this catch-all covers. Root catch-all → `''`
383
+ * (matches everything as last resort). Longest segment-prefix wins at match
384
+ * time. Never contains `*` (it's the parent prefix, not the catch-all path). */
385
+ notFoundPrefix?: string;
386
+ }
387
+ /** Compose a child's relative path onto a parent's base path.
388
+ * - `rel === ''` (layout-only child) → returns `base` unchanged
389
+ * - `rel` starts with `/` (absolute) → returns `rel` (only valid when base === '';
390
+ * flattenRoutes rejects this case otherwise)
391
+ * - otherwise → strip any trailing `/` from base, append `/${rel}` */
392
+ export declare function joinPath(base: string, rel: string): string;
393
+ /** Walk the nested route tree, emitting one FlatRoute per leaf or index node.
394
+ * Composes paths, middleware, errorBoundary, and cache per the rules in
395
+ * the design spec (S3). */
396
+ export declare function flattenRoutes(routes: Route[]): FlatRoute[];
397
+ /** Internal React context that carries the next-deeper rendered element to
398
+ * the parent's <Outlet />. Default `null` means "no child to render" —
399
+ * `<Outlet />` from a leaf route or a flat route renders nothing. */
400
+ export declare const OutletContext: import("react").Context<ReactNode>;
401
+ /** Renders the matched child route inside a parent layout. Read via
402
+ * React context; falls back to null at the leaf or in a flat (non-nested)
403
+ * route. Use inside a parent Component:
404
+ *
405
+ * function AdminLayout() {
406
+ * return <div><nav>…</nav><main><Outlet /></main></div>
407
+ * }
408
+ */
409
+ export declare function Outlet(): ReactNode;
410
+ /** Process the user's nested route tree into a flat array for the renderer
411
+ * and Rust route table. Each leaf/index node becomes one FlatRoute. Indices
412
+ * are stable across worker reloads (= array position), matching Rust's
413
+ * route_id semantics. */
414
+ export declare function defineRoutes(routes: Route[]): FlatRoute[];
415
+ /** Wire-level shape of the JSON envelope produced by Rust. Discriminated
416
+ * union: render path (matched against a route) vs action path
417
+ * (POST /_brust/action/<id>). Keep these in sync with src/routes.rs
418
+ * RouteEnvelope / ActionEnvelope.
419
+ */
420
+ export type RouteCall = {
421
+ kind: 'render';
422
+ route_id: number;
423
+ path: string;
424
+ params: Record<string, string>;
425
+ req: BrustRequest;
426
+ /** Set by Rust when the route's `cache.bypass` matched — routes this
427
+ * request to the L2 programmatic cache (worker `cache.key`). */
428
+ bypassed?: boolean;
429
+ } | {
430
+ kind: 'navigation';
431
+ route_id: number;
432
+ path: string;
433
+ params: Record<string, string>;
434
+ req: BrustRequest;
435
+ } | {
436
+ kind: 'action';
437
+ action_id: string;
438
+ /** Request's Content-Type, whitespace-trimmed (case preserved by Rust;
439
+ * the dispatch points below lowercase defensively). '' means the header
440
+ * was missing. */
441
+ content_type: string;
442
+ /** UTF-8 text body — present for application/json and
443
+ * application/x-www-form-urlencoded. Mutually exclusive with body_b64. */
444
+ body_text?: string;
445
+ /** Base64-encoded binary body — present for multipart/form-data.
446
+ * JS decodes via Buffer.from(s, 'base64') before parsing. */
447
+ body_b64?: string;
448
+ /** Path params extracted by the Rust router (e.g. {id} → "abc"). */
449
+ params?: Record<string, string>;
450
+ req: BrustRequest;
451
+ } | {
452
+ kind: 'mcp';
453
+ body_text: string;
454
+ req: BrustRequest;
455
+ } | {
456
+ kind: 'sse';
457
+ conn_id: bigint;
458
+ req: BrustRequest;
459
+ } | {
460
+ kind: 'ws';
461
+ conn_id: bigint;
462
+ client_subprotocols: string[];
463
+ req: BrustRequest;
464
+ };
465
+ /**
466
+ * Build a render callback for a given routes table. The returned function is
467
+ * what gets passed to `brust.registerRenderer(view, fn)` on the worker side.
468
+ *
469
+ * Wire format written to the SAB: [meta_len: u16 BE][meta JSON UTF-8][body bytes].
470
+ * meta = { status: number, headers?: Record<string, string> }.
471
+ */
472
+ export interface MakeRendererOptions {
473
+ /** Lazy getter for the Bun Worker id. Called per-render so the value can be
474
+ * resolved after `registerRenderer` returns. Returns null before that. */
475
+ getWorkerId?: () => number | null;
476
+ /** Action table the worker dispatches to when envelope.kind === 'action'.
477
+ * Both the main process and each worker call `brust.scanActions(...)` at
478
+ * module top-level and pass the resulting array here — the wire keys (ids)
479
+ * and the handler functions (fn) must agree across both ends. */
480
+ actions?: EndpointDef[];
481
+ /** MCP server instance — built once per worker at module top-level. */
482
+ mcp?: import('./mcp/server.ts').McpServer;
483
+ /** Render slots per worker. The `view` SAB is partitioned into this many
484
+ * disjoint sub-views; each render is dispatched with a `slot` index and
485
+ * operates on `view.subarray(slot*sub, slot*sub+sub)`. Default 1 (the whole
486
+ * view → byte-identical to the pre-multi-slot path). */
487
+ slots?: number;
488
+ }
489
+ export declare function makeRenderer(routes: FlatRoute[], view: Uint8Array, opts?: MakeRendererOptions): (envelopeJson: string, slot?: number) => Promise<number>;
490
+ /** Plain response object shape returned by action/mcp branches and consumed
491
+ * by `emitSingleChunkResponse`. The branches no longer write to the SAB
492
+ * directly — they hand back a JS-side response and the caller packs +
493
+ * dispatches it through the chunk channel. */
494
+ interface BranchResponse {
495
+ status: number;
496
+ contentType: string;
497
+ body: string | Uint8Array;
498
+ headers?: Record<string, string>;
499
+ }
500
+ export declare function dispatchAction(call: Extract<RouteCall, {
501
+ kind: 'action';
502
+ }>, byId: Map<string, EndpointDef>): Promise<BranchResponse>;
503
+ /** Right-to-left compose a middleware chain. Each middleware wraps the next;
504
+ * the terminal step ends up at the innermost call. Returning without calling
505
+ * next() short-circuits. Used identically by render + action branches. */
506
+ export declare function composeChain(req: BrustRequest, mws: Middleware[] | undefined, terminal: () => Promise<RouteResponse>): () => Promise<RouteResponse>;
@@ -0,0 +1,22 @@
1
+ import type { Route, RouteCall } from '../routes.ts';
2
+ export type SseCall = Extract<RouteCall, {
3
+ kind: 'sse';
4
+ }>;
5
+ /** NAPI surface — Rust provides these. In tests, a mock satisfies the shape. */
6
+ export interface SseNapi {
7
+ write(conn_id: bigint, bytes: Uint8Array): Promise<void>;
8
+ close(conn_id: bigint): void;
9
+ registerAbort(conn_id: bigint, cb: () => void): void;
10
+ signalOpen(conn_id: bigint, status: number, body: string, contentType: string): void;
11
+ }
12
+ /**
13
+ * Per-connection JS driver for SSE routes. Called once per client connection.
14
+ *
15
+ * Contract with the Rust napi shim:
16
+ * - napi.registerAbort(conn_id, cb) — Rust calls cb when client disconnects
17
+ * - napi.signalOpen(conn_id, status, body, contentType)
18
+ * — JS reports verdict; Rust waits before writing headers
19
+ * - napi.write(conn_id, bytes) — write a chunk; Promise resolves when TCP write completes (backpressure)
20
+ * - napi.close(conn_id) — JS tells Rust to tear down the per-conn task
21
+ */
22
+ export declare function handleSseStream(call: SseCall, route: Route, napi: SseNapi): Promise<void>;
@@ -0,0 +1,31 @@
1
+ /** Minimal Standard Schema v1 surface — https://standardschema.dev */
2
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
3
+ readonly '~standard': {
4
+ readonly version: 1;
5
+ readonly vendor: string;
6
+ readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
7
+ };
8
+ }
9
+ type StandardResult<Output> = {
10
+ readonly value: Output;
11
+ readonly issues?: undefined;
12
+ } | {
13
+ readonly issues: ReadonlyArray<StandardIssue>;
14
+ };
15
+ export interface StandardIssue {
16
+ readonly message: string;
17
+ readonly path?: ReadonlyArray<PropertyKey | {
18
+ key: PropertyKey;
19
+ }>;
20
+ }
21
+ export type InferOutput<S> = S extends StandardSchemaV1<unknown, infer O> ? O : never;
22
+ export type ValidateOk<T> = {
23
+ ok: true;
24
+ value: T;
25
+ };
26
+ export type ValidateErr = {
27
+ ok: false;
28
+ issues: ReadonlyArray<StandardIssue>;
29
+ };
30
+ export declare function validate<S extends StandardSchemaV1 | undefined>(schema: S, input: unknown): Promise<ValidateOk<S extends StandardSchemaV1 ? InferOutput<S> : unknown> | ValidateErr>;
31
+ export {};
@@ -0,0 +1,31 @@
1
+ import { type Computed, type Signal } from './signal.ts';
2
+ export interface StoreInstanceRecord {
3
+ instance: object;
4
+ subs: Set<() => void>;
5
+ version: {
6
+ n: number;
7
+ };
8
+ snap: {
9
+ value: Record<string, unknown>;
10
+ version: number;
11
+ } | null;
12
+ handle?: {
13
+ serialize(): Record<string, unknown>;
14
+ };
15
+ }
16
+ type ServerResolver = (name: string, create: () => StoreInstanceRecord) => StoreInstanceRecord;
17
+ export declare function __setServerResolver(fn: ServerResolver): void;
18
+ type StoreValue<X> = X extends Signal<infer T> ? T : X extends Computed<infer T> ? T : never;
19
+ export type Snapshot<S> = {
20
+ [K in keyof S as [StoreValue<S[K]>] extends [never] ? S[K] extends (...a: never[]) => unknown ? never : K : K]: [StoreValue<S[K]>] extends [never] ? S[K] : StoreValue<S[K]>;
21
+ };
22
+ export interface StoreHandle<S extends object> {
23
+ (): S;
24
+ readonly name: string;
25
+ subscribe(cb: () => void): () => void;
26
+ snapshot(): Snapshot<S>;
27
+ serialize(): Record<string, unknown>;
28
+ hydrate(state: Record<string, unknown>): void;
29
+ }
30
+ export declare function defineStore<S extends object>(name: string, factory: () => S): StoreHandle<S> & S;
31
+ export {};
@@ -0,0 +1,5 @@
1
+ export { signal, computed, effect, batch, isSignal, isComputed } from './signal.ts';
2
+ export type { Signal, Computed } from './signal.ts';
3
+ export { defineStore } from './define-store.ts';
4
+ export type { StoreHandle, Snapshot } from './define-store.ts';
5
+ export { toScriptJson, parseStoreScript, storeScriptTag } from './serialize.ts';
@@ -0,0 +1,2 @@
1
+ import type { Snapshot, StoreHandle } from './define-store.ts';
2
+ export declare function useStore<S extends object>(store: StoreHandle<S> & S): Snapshot<S>;
@@ -0,0 +1,5 @@
1
+ export declare function toScriptJson(value: unknown): string;
2
+ export declare function storeScriptTag(name: string, state: unknown): string;
3
+ export declare function parseStoreScript(el: {
4
+ textContent: string | null;
5
+ }): Record<string, unknown>;
@@ -0,0 +1,4 @@
1
+ import { type StoreInstanceRecord } from './define-store.ts';
2
+ export declare function runInStoreContext<T>(fn: () => T): T;
3
+ export declare function getServerInstance(name: string, create: () => StoreInstanceRecord): StoreInstanceRecord;
4
+ export declare function collectSnapshot(): Record<string, Record<string, unknown>> | null;
@@ -0,0 +1,18 @@
1
+ declare const SIGNAL: unique symbol;
2
+ declare const COMPUTED: unique symbol;
3
+ export interface Signal<T> {
4
+ (): T;
5
+ set(next: T | ((prev: T) => T)): void;
6
+ readonly [SIGNAL]: true;
7
+ }
8
+ export interface Computed<T> {
9
+ (): T;
10
+ readonly [COMPUTED]: true;
11
+ }
12
+ export declare function isSignal(v: unknown): v is Signal<unknown>;
13
+ export declare function isComputed(v: unknown): v is Computed<unknown>;
14
+ export declare function batch(fn: () => void): void;
15
+ export declare function signal<T>(initial: T): Signal<T>;
16
+ export declare function computed<T>(fn: () => T): Computed<T>;
17
+ export declare function effect(fn: () => void | (() => void)): () => void;
18
+ export {};
@@ -0,0 +1,18 @@
1
+ export declare const templates: {
2
+ /** Register (or replace) a runtime template under `name`. Names are opaque
3
+ * keys (`shop/42/section/7@v3` is fine). Throws on jinja syntax errors
4
+ * (message includes line info). Replacement is atomic: concurrent renders
5
+ * see old or new, never missing. */
6
+ register(name: string, jinjaSource: string): void;
7
+ /** Remove a runtime-registered template. Returns whether it existed.
8
+ * Boot-tier templates (compiled from routes) are not removable. */
9
+ remove(name: string): boolean;
10
+ /** True when `name` resolves in either tier (dynamic first, then boot). */
11
+ has(name: string): boolean;
12
+ /** Names of runtime-registered templates (dynamic tier only). */
13
+ list(): string[];
14
+ /** Render a template (either tier) to an HTML string. Pure (name, data) →
15
+ * html — no request/store context. NOT the request fast lane; intended for
16
+ * handlers/loaders/tooling (draft canvases, section previews). */
17
+ render(name: string, data?: unknown): string;
18
+ };