brustjs 0.1.49-alpha → 0.1.51-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 (93) 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/build.ts +7 -0
  5. package/runtime/cli/dev.ts +7 -0
  6. package/runtime/cli/native-routes-emit.ts +147 -1
  7. package/runtime/cli/ssg.ts +94 -23
  8. package/runtime/config.ts +42 -0
  9. package/runtime/index.d.ts +63 -0
  10. package/runtime/index.js +57 -52
  11. package/runtime/index.ts +114 -9
  12. package/runtime/islands/page-cache.ts +32 -2
  13. package/runtime/native/runtime.ts +220 -7
  14. package/runtime/render/fragment.ts +87 -0
  15. package/runtime/routes.ts +482 -95
  16. package/runtime/templates.ts +47 -0
  17. package/runtime/treaty.ts +24 -1
  18. package/types/action-error.d.ts +18 -0
  19. package/types/cache-sync.d.ts +42 -0
  20. package/types/cache.d.ts +20 -0
  21. package/types/cli/help.d.ts +28 -0
  22. package/types/cli/jinja-staleness.d.ts +14 -0
  23. package/types/cli/native-routes-emit.d.ts +217 -0
  24. package/types/cli/new.d.ts +30 -0
  25. package/types/cli/templates.d.ts +39 -0
  26. package/types/client/index.d.ts +14 -0
  27. package/types/config.d.ts +42 -0
  28. package/types/cookies.d.ts +25 -0
  29. package/types/create.d.ts +1 -0
  30. package/types/css/build.d.ts +11 -0
  31. package/types/css/component-build.d.ts +17 -0
  32. package/types/css/component-loader.d.ts +8 -0
  33. package/types/css/manifest.d.ts +21 -0
  34. package/types/css/process-modules.d.ts +31 -0
  35. package/types/css/route-deps.d.ts +20 -0
  36. package/types/css/scan-imports.d.ts +13 -0
  37. package/types/css.d.ts +16 -0
  38. package/types/define-actions.d.ts +133 -0
  39. package/types/dev/client.d.ts +8 -0
  40. package/types/dev/coordinator.d.ts +33 -0
  41. package/types/dev/inject.d.ts +6 -0
  42. package/types/dev/jinja-reload.d.ts +7 -0
  43. package/types/dev/tui.d.ts +35 -0
  44. package/types/dev/watcher.d.ts +34 -0
  45. package/types/dev/worker-registry.d.ts +17 -0
  46. package/types/dev/ws-channel.d.ts +39 -0
  47. package/types/generator.d.ts +23 -0
  48. package/types/index.d.ts +222 -0
  49. package/types/islands/brust-page.d.ts +74 -0
  50. package/types/islands/build.d.ts +49 -0
  51. package/types/islands/chunk-id.d.ts +10 -0
  52. package/types/islands/importmap.d.ts +2 -0
  53. package/types/islands/island.d.ts +65 -0
  54. package/types/islands/isr-jsx.d.ts +31 -0
  55. package/types/islands/native-render.d.ts +89 -0
  56. package/types/loader-cache.d.ts +18 -0
  57. package/types/mcp/extractor.d.ts +14 -0
  58. package/types/mcp/manifest.d.ts +23 -0
  59. package/types/mcp/schema.d.ts +19 -0
  60. package/types/mcp/server.d.ts +15 -0
  61. package/types/md/emit.d.ts +72 -0
  62. package/types/md/render.d.ts +80 -0
  63. package/types/md/routes.d.ts +119 -0
  64. package/types/md/scan.d.ts +34 -0
  65. package/types/md/slug.d.ts +1 -0
  66. package/types/native/build.d.ts +30 -0
  67. package/types/native/index.d.ts +2 -0
  68. package/types/native/runtime.d.ts +52 -0
  69. package/types/navigation/active-nav.d.ts +2 -0
  70. package/types/navigation/index.d.ts +5 -0
  71. package/types/navigation/navigate.d.ts +14 -0
  72. package/types/navigation/react.d.ts +15 -0
  73. package/types/navigation/store.d.ts +44 -0
  74. package/types/render/fragment.d.ts +20 -0
  75. package/types/render/inject-action-prefix.d.ts +9 -0
  76. package/types/render/inject-css-link.d.ts +8 -0
  77. package/types/render/inject-dev-client.d.ts +6 -0
  78. package/types/render/inject-generator.d.ts +7 -0
  79. package/types/render/inject-store.d.ts +9 -0
  80. package/types/render/stream.d.ts +45 -0
  81. package/types/request-context.d.ts +16 -0
  82. package/types/routes.d.ts +506 -0
  83. package/types/sse/handler.d.ts +22 -0
  84. package/types/standard-schema.d.ts +31 -0
  85. package/types/store/define-store.d.ts +31 -0
  86. package/types/store/index.d.ts +5 -0
  87. package/types/store/react.d.ts +2 -0
  88. package/types/store/serialize.d.ts +5 -0
  89. package/types/store/server-context.d.ts +4 -0
  90. package/types/store/signal.d.ts +18 -0
  91. package/types/templates.d.ts +18 -0
  92. package/types/treaty.d.ts +70 -0
  93. package/types/ws/handler.d.ts +26 -0
@@ -0,0 +1,222 @@
1
+ import * as native from './index.js';
2
+ /** Global CORS policy (one policy for the whole deployment — pages, actions,
3
+ * static assets alike; brust has no per-route CORS). Opt-in: omit to keep CORS
4
+ * disabled (byte-identical default — no `Access-Control-*` headers, OPTIONS
5
+ * still 405). Threaded into Rust, which answers preflights before the render
6
+ * pipeline and stamps actual responses at a single chokepoint. */
7
+ export interface CorsOptions {
8
+ /** Allowed origins, exact scheme+host+port match (no wildcard subdomains).
9
+ * A list CONTAINING `'*'` is treated as wildcard: every origin allowed,
10
+ * echoed as the literal `*`. */
11
+ origins: string[];
12
+ /** Preflight `Access-Control-Allow-Methods`. Default: GET,POST,PUT,PATCH,DELETE,OPTIONS. */
13
+ methods?: string[];
14
+ /** Preflight `Access-Control-Allow-Headers`. Default: echo the request's
15
+ * `Access-Control-Request-Headers`. */
16
+ headers?: string[];
17
+ /** `Access-Control-Expose-Headers` on actual responses. Default: none. */
18
+ exposeHeaders?: string[];
19
+ /** Emit `Access-Control-Allow-Credentials: true`. INVALID combined with a
20
+ * wildcard origin — serve() throws at boot (browsers silently reject
21
+ * `Allow-Credentials` with `Allow-Origin: *`; we make it loud). */
22
+ credentials?: boolean;
23
+ /** Preflight `Access-Control-Max-Age` seconds. Default 600. */
24
+ maxAgeSeconds?: number;
25
+ }
26
+ /** Fail-fast CORS validation, mirroring Rust's `CorsConfig::validate` so a bad
27
+ * config throws in TS before the native call (clearer stack, same message
28
+ * shape). Exported for unit testing. */
29
+ export declare function validateCorsOptions(cors: CorsOptions): void;
30
+ export interface ServeOptions {
31
+ /** Host/address to bind on. A hostname (e.g. `localhost`, resolved Rust-side)
32
+ * or a literal IP such as `0.0.0.0` / `127.0.0.1`. */
33
+ host: string;
34
+ port: number;
35
+ workers: number;
36
+ entry: string;
37
+ bootTimeoutMs?: number;
38
+ /** Actions builder from `defineActions(...)`. When present, `serve`
39
+ * registers its endpoints (method/path, keyed by registration index)
40
+ * before the listener binds. Optional — omit if the app has no actions. */
41
+ actions?: import('./define-actions.ts').ActionsBuilder;
42
+ /** URL prefix the action router mounts under (e.g. `/_actions`). Threaded
43
+ * into Rust's ServeOptions.action_prefix. */
44
+ actionPrefix?: string;
45
+ /** Optional global CORS policy — see {@link CorsOptions}. Validated at boot
46
+ * (TS first, Rust mirrors): `origins` non-empty; `credentials` + wildcard
47
+ * origin throws. */
48
+ cors?: CorsOptions;
49
+ /** MCP support — pass a manifest built via brust.buildMcpManifest. brust.serve
50
+ * does NOT auto-wire MCP into workers; the worker branch of the entry file must
51
+ * call brust.loadMcpManifest() + makeMcpServer() itself and pass the McpServer
52
+ * to makeRenderer via `opts.mcp`. See example/pokedex/index.ts for the
53
+ * pattern. The field here is currently unused inside serve() and is reserved
54
+ * for future IPC-based propagation of the manifest. */
55
+ mcp?: {
56
+ manifest: import('./mcp/manifest.ts').McpManifest;
57
+ };
58
+ /** Performance tunables. All optional — omitting any field keeps the
59
+ * framework default, so the whole object is opt-in. `connWorkers` sets the
60
+ * Rust-side connection-handling concurrency (accept→parse→dispatch), which is
61
+ * INDEPENDENT of `workers` (the Bun render threads): I/O-bound paths like
62
+ * `/ping` scale with `connWorkers`, render-bound paths scale with `workers`.
63
+ * Setting `connWorkers > workers` is safe: a render dispatch that finds every
64
+ * worker busy QUEUES (awaits a free worker up to `claimTimeoutMs`) instead of
65
+ * 503-ing, so excess conn-workers just wait their turn rather than dropping
66
+ * requests. */
67
+ tuning?: {
68
+ /** Rust accept/dispatch concurrency. Default = `workers`. */
69
+ connWorkers?: number;
70
+ /** Max request bytes before header terminator. Default 16384 (16 KB). */
71
+ maxRequestBytes?: number;
72
+ /** Max action/RPC body size. Default 262144 (256 KB). */
73
+ maxActionBodyBytes?: number;
74
+ /** Accept-side queue depth (TCP backpressure point). Default 1024. */
75
+ connQueueCap?: number;
76
+ /** Initial per-connection read buffer capacity. Default 4096. */
77
+ readBufCap?: number;
78
+ /** Max ms a render waits for a free worker before 503 (AllBusy queues
79
+ * instead of failing fast). Default 10000. */
80
+ claimTimeoutMs?: number;
81
+ /** tokio I/O runtime worker-thread count for the hyper server. Runs inside
82
+ * Bun (which has its own threads + render workers), so this is NOT
83
+ * one-per-core. Default `min(availableParallelism, 4)` (fallback 2). */
84
+ workerThreads?: number;
85
+ /** Render slots per Bun worker — concurrent in-flight renders per isolate.
86
+ * Default `min(cores, 16)` (set `BRUST_RENDER_SLOTS` to go higher on >16-core
87
+ * hosts). Only speeds renders that `await` during render (e.g. Suspense with
88
+ * async data); CPU-bound/native/cache routes serialize on the one isolate and
89
+ * are unaffected. The count is propagated to each worker via the
90
+ * `BRUST_RENDER_SLOTS` env var and scales the per-worker SAB (one region per
91
+ * slot, so the cap bounds memory). slots > 1 is byte-identical to slots = 1. */
92
+ renderSlots?: number;
93
+ };
94
+ }
95
+ export type RenderFn = (envelopeJson: string, slot: number) => Promise<number>;
96
+ export declare const isWorker: boolean;
97
+ export declare function workerId(): number | null;
98
+ export declare const brust: {
99
+ serve(opts: ServeOptions): Promise<void>;
100
+ /** Install the route table in Rust. MUST be called before `serve()`.
101
+ * Pass an array of FlatRoutes from `defineRoutes(...)` — each is JSON-encoded
102
+ * with its optional cache config. Rust matches against `fullPath`. */
103
+ registerRoutes(routes: import("./routes.ts").FlatRoute[]): number;
104
+ /** Register the list of literal route paths that should be dispatched as
105
+ * SSE (text/event-stream) instead of going through the render pipeline.
106
+ * Call from the main process after defineRoutes — the worker only needs
107
+ * the call if it also accepts SSE traffic, but in the current model the
108
+ * main accept loop owns dispatch so this is main-only. MVP supports only
109
+ * literal paths; parameterized routes (e.g. `/sse/{room}`) are a follow-up. */
110
+ registerSsePaths(paths: string[]): void;
111
+ /** Register the list of literal route paths that should be dispatched as
112
+ * WebSocket upgrades. Call from the main process after defineRoutes.
113
+ * MVP supports only literal paths — parameterized routes (e.g.
114
+ * `/ws/chat/{room}`) are a follow-up. */
115
+ registerWsPaths(paths: string[]): void;
116
+ /** Set the L1 response-cache and L2 page-cache capacities (entries).
117
+ * Default is 1000 each. Rust reconstructs both caches at the given
118
+ * capacities (moka fixes capacity at construction), so call this once at
119
+ * boot before serving begins. */
120
+ configureCache(opts: {
121
+ maxEntries: number;
122
+ pageMaxEntries: number;
123
+ }): void;
124
+ /** Tell Rust where to read `/_brust/islands/<file>` from. Called once at
125
+ * boot after buildIslands() emits chunks. Path must be absolute. */
126
+ configureIslandsDir(dir: string): void;
127
+ /** Tell Rust where to read `/_brust/css/<file>` from. Called from the
128
+ * main thread when CSS is configured. Path must be absolute. */
129
+ configureCssDir(dir: string): void;
130
+ /** Tell Rust where to read root-mapped static assets (`/favicon.ico`, …) from.
131
+ * Path must be absolute. */
132
+ configurePublicDir(dir: string): void;
133
+ /** Extract the MCP manifest from TypeScript source using the compiler API,
134
+ * write it to `.brust/mcp-manifest.json`, and return it. Call once in the
135
+ * main process after `brust.registerRoutes(routes)`. Workers must read the
136
+ * persisted manifest themselves via `brust.loadMcpManifest()` in the worker
137
+ * branch and pass an `McpServer` (built via `makeMcpServer`) to
138
+ * `makeRenderer` via `opts.mcp`. See example/pokedex/index.ts. */
139
+ buildMcpManifest(opts: {
140
+ actionsFile?: string;
141
+ routesFile: string;
142
+ sourceRoots: string[];
143
+ routes: import("./routes.ts").FlatRoute[];
144
+ cwd?: string;
145
+ }): Promise<import("./mcp/manifest.ts").McpManifest>;
146
+ /** Read the MCP manifest from `.brust/mcp-manifest.json`. Returns null if
147
+ * the file does not exist (i.e. the main process hasn't called
148
+ * `brust.buildMcpManifest` yet). Throws if the file is malformed. */
149
+ loadMcpManifest(cwd?: string): Promise<import("./mcp/manifest.ts").McpManifest | null>;
150
+ registerRenderer(buf: Uint8Array, slots: number, fn: RenderFn): number;
151
+ /**
152
+ * One-call lifecycle: scans actions, builds islands (if `routes.tsx` uses
153
+ * `<Island>`), registers routes + SSE/WS paths, builds the MCP manifest, then
154
+ * branches to `serve()` (main thread) or registers a renderer (worker
155
+ * thread). Replaces ~70 lines of boilerplate; the lower-level helpers
156
+ * (`scanActions`, `registerRoutes`, `makeRenderer`, etc.) are still exported
157
+ * for apps that need finer control.
158
+ *
159
+ * Conventions assumed (override via the corresponding option if your layout
160
+ * differs):
161
+ * - `<scanRoot>/routes.tsx` — scanned for `<Island>` usage (islands)
162
+ * and referenced by the MCP manifest builder
163
+ *
164
+ * `scanRoot` defaults to the directory of `entry` so the typical caller
165
+ * passes only `{ routes, entry: import.meta.url }`.
166
+ */
167
+ run(opts: {
168
+ routes: import("./routes.ts").FlatRoute[];
169
+ entry: string;
170
+ scanRoot?: string;
171
+ /** Host/address to bind on. Default `localhost`. A `brust dev --port` /
172
+ * BRUST_PORT / BRUST_ADDR / brust.toml all override this app-level value
173
+ * (precedence: env > toml > this > framework default). */
174
+ address?: string;
175
+ /** TCP port to bind on. Default 1337. Overridable by env/toml — see
176
+ * `address`. */
177
+ port?: number;
178
+ /** Actions builder from `defineActions(...)`. Registered with Rust and
179
+ * threaded to each worker's renderer. Omit if the app has no actions. */
180
+ actions?: import("./define-actions.ts").ActionsBuilder;
181
+ /** URL prefix the action router mounts under. Threaded to serve(). */
182
+ actionPrefix?: string;
183
+ /** Optional global CORS policy — see {@link CorsOptions}. Threaded to
184
+ * serve() like `actionPrefix`. */
185
+ cors?: CorsOptions;
186
+ /** Overrides merged into the underlying `serve()` call (main thread). */
187
+ serve?: Partial<Omit<ServeOptions, "entry" | "actions" | "mcp">>;
188
+ /** Per-worker SAB size in bytes. Default 256 KB. */
189
+ sabBytes?: number;
190
+ /** When true, prepend the dev WS route, install file watcher, set the
191
+ * dev-client snippet, and start the TUI. Default false.
192
+ * Also activated by BRUST_DEV=1 environment variable. */
193
+ dev?: boolean;
194
+ }): Promise<void>;
195
+ };
196
+ export { defineRoutes, makeRenderer, Outlet, notFound, redirect, isNativeVerdict, httpError, isHttpErrorTrigger, } from './routes.ts';
197
+ export type { Route, RouteCall, RouteContext, ErrorBoundaryProps, RouteCacheConfig, BrustRequest, RouteResponse, Middleware, NativeVerdict, HttpErrorOpts, HttpErrorTrigger, } from './routes.ts';
198
+ export { defineActions, isValidEndpointPath } from './define-actions.ts';
199
+ export type { EndpointDef, ActionContext, EndpointOptions, ActionsBuilder, } from './define-actions.ts';
200
+ export { ActionError, isActionError } from './action-error.ts';
201
+ export type { ActionErrorBody } from './action-error.ts';
202
+ export { loadConfig, BrustConfigError } from './config.ts';
203
+ export type { BrustConfig } from './config.ts';
204
+ export { Island } from './islands/island.tsx';
205
+ export type { IslandProps, HydrateTrigger } from './islands/island.tsx';
206
+ import './islands/isr-jsx.ts';
207
+ export type { IsrConfig } from './islands/isr-jsx.ts';
208
+ export { BrustPage } from './islands/brust-page.tsx';
209
+ export type { BrustPageProps, HeadEntry } from './islands/brust-page.tsx';
210
+ export { defineStore, signal, computed, effect, batch } from './store/index.ts';
211
+ export type { StoreHandle, Snapshot, Signal, Computed } from './store/index.ts';
212
+ export { dedupe, cachedFetch } from './loader-cache.ts';
213
+ export { renderFragment } from './render/fragment.ts';
214
+ export type { RenderFragmentOpts } from './render/fragment.ts';
215
+ export { buildIslands } from './islands/build.ts';
216
+ export type { IslandsBuildResult, BuildIslandsOptions } from './islands/build.ts';
217
+ export { cache } from './cache.ts';
218
+ export type { InvalidateArgs } from './cache.ts';
219
+ export { templates } from './templates.ts';
220
+ export { getRequestContext } from './request-context.ts';
221
+ export { cookies } from './cookies.ts';
222
+ export type { CookieOptions } from './cookies.ts';
@@ -0,0 +1,74 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Props for the built-in `<BrustPage>` document shell.
3
+ *
4
+ * `<BrustPage>` is a NATIVE-route document component: in a `native: true` route
5
+ * the Rust JSX compiler intercepts the `BrustPage` tag and emits the whole
6
+ * `<html>/<head>/<body>` skeleton itself, auto-injecting the framework head tags
7
+ * (charset, viewport, the `/_brust/css/app.css` stylesheet link). The head is
8
+ * configured ENTIRELY through these props — you never write `<head>` markup, so
9
+ * brust keeps full ownership of `<head>` and can add more tags later (importmap,
10
+ * preloads) without colliding with hand-written head elements.
11
+ *
12
+ * On the native path each prop accepts a compile-time string literal OR a
13
+ * loader member-path (`title={data.title}`), interpolated into the Rust-rendered
14
+ * shell as `{{ path }}` (S8). Calls/arithmetic are still rejected. This React
15
+ * implementation mirrors the compiled output for the rare non-native use. */
16
+ /** One extra `<head>` element for `<BrustPage head={[…]}>`. Discriminated by
17
+ * `tag`. On the native path, attribute values may be a string literal or a
18
+ * loader member-path (HTML-escaped); `text` (inner content of style/script/
19
+ * noscript) must be a static string literal (raw — never user input). */
20
+ export type HeadEntry = {
21
+ tag: 'link';
22
+ rel: string;
23
+ href: string;
24
+ type?: string;
25
+ sizes?: string;
26
+ as?: string;
27
+ media?: string;
28
+ crossOrigin?: string;
29
+ } | {
30
+ tag: 'meta';
31
+ name?: string;
32
+ property?: string;
33
+ httpEquiv?: string;
34
+ content: string;
35
+ } | {
36
+ tag: 'base';
37
+ href?: string;
38
+ target?: string;
39
+ } | {
40
+ tag: 'style';
41
+ text: string;
42
+ media?: string;
43
+ } | {
44
+ tag: 'script';
45
+ src?: string;
46
+ text?: string;
47
+ type?: string;
48
+ defer?: boolean;
49
+ async?: boolean;
50
+ crossOrigin?: string;
51
+ } | {
52
+ tag: 'noscript';
53
+ text: string;
54
+ };
55
+ export interface BrustPageProps {
56
+ /** `<html lang>` — defaults to `"en"`. */
57
+ lang?: string;
58
+ /** `<html class>` (e.g. `"dark"`). */
59
+ className?: string;
60
+ /** `<body class>`. */
61
+ bodyClassName?: string;
62
+ /** `<title>…</title>`. Omitted when absent. */
63
+ title?: string;
64
+ /** `<meta name="description" content="…">`. Omitted when absent. */
65
+ description?: string;
66
+ /** Extra `<head>` elements (link/meta/base/style/script/noscript). */
67
+ head?: HeadEntry[];
68
+ /** Page body — rendered inside `<body>`. */
69
+ children?: ReactNode;
70
+ /** Arbitrary `data-*` on `<html>` (e.g. `data-mode="dark"`). String literal
71
+ * or loader member-path on the native path. */
72
+ [dataAttr: `data-${string}`]: string | undefined;
73
+ }
74
+ export declare function BrustPage({ lang, className, bodyClassName, title, description, head, children, ...rest }: BrustPageProps): ReactNode;
@@ -0,0 +1,49 @@
1
+ import type { BunPlugin } from 'bun';
2
+ export interface IslandsBuildResult {
3
+ /** Absolute path to the output directory passed to brust's Rust side. */
4
+ outDir: string;
5
+ /** Number of island chunks emitted (excludes runtime + bootstrap). */
6
+ islandCount: number;
7
+ /** id → content-addressed chunk URL (`/_brust/islands/<id>_<hash>.js`). Also
8
+ * written to `_islands.js` for the client bootstrap to resolve at runtime. */
9
+ chunks: Record<string, string>;
10
+ }
11
+ export interface BuildIslandsOptions {
12
+ /** Override the output directory. Default: `<cwd>/.brust/islands`. */
13
+ outDir?: string;
14
+ /** Build plugins passed straight to `Bun.build` for the per-island chunks.
15
+ * Needed for the component-CSS loader: global `Bun.plugin()` registrations do
16
+ * NOT apply to `Bun.build`, so an island that `import`s a `.module.css` must
17
+ * get the resolver here or Bun emits the CSS as a separate asset and collides
18
+ * on the output filename (X.module.css + X.tsx → both X.js). */
19
+ plugins?: BunPlugin[];
20
+ }
21
+ /** Scan a routes entry file for `<Island component={X} />` usage and derive the
22
+ * island chunk list (componentName → absolute source path). Replaces the old
23
+ * static config-file lookup — the chunk set is derived from source.
24
+ *
25
+ * 1. Resolve the entry's page imports via {@link scanImports}.
26
+ * 2. For each page, slice every `<Island … />` tag and capture its
27
+ * `component={Ident}`. A tag with no `component` is a hard error (F3:
28
+ * never silently skip an island).
29
+ * 3. Resolve each captured ident through that page's OWN imports.
30
+ * 4. Key by the content-addressed {@link islandChunkBasename} (`<Name>_<hash>`),
31
+ * NOT the bare name — so two DIFFERENT files exporting a same-named component
32
+ * produce two distinct chunks. Same name + same file dedups (same id). The
33
+ * marker carries this same id (native: reconcileIslandManifest rewrite;
34
+ * React: the Component→id registry seeded at worker boot), so there is no
35
+ * app-unique-name requirement.
36
+ *
37
+ * `extraIslands` (task 2.8) merges additional islands the routes-graph scan
38
+ * cannot see — md-route islands resolved by `emitMdTemplates` (`name →
39
+ * absolute source path`, the bare-name map it returns). Each is keyed by the
40
+ * same content-addressed id, so same name + same path dedups against the scan
41
+ * result; same name + different path yields a distinct id (two chunks, the
42
+ * shipped same-name parity). A same-id-different-path collision is a hard
43
+ * error — never silently rebind a chunk id.
44
+ */
45
+ export declare function scanIslandChunks(routesEntryFile: string, extraIslands?: Map<string, string>): Map<string, string>;
46
+ /** Build the runtime chunks + all island chunks + bootstrap. Returns the
47
+ * absolute output directory; caller passes it to `brust.configureIslandsDir`. */
48
+ export declare function buildIslands(islands: Map<string, string>, options?: BuildIslandsOptions): Promise<IslandsBuildResult>;
49
+ export declare function buildOne(entrypoints: string[], outdir: string, naming: string, external: string[], plugins?: BunPlugin[]): Promise<void>;
@@ -0,0 +1,10 @@
1
+ /** Content-addressed island id / chunk basename = `<Name>_<8hex(sha256
2
+ * cwd-relative source path)>`. The SINGLE name contract (mirrors the directive
3
+ * chunk scheme): the chunk filename, the `data-brust-island` marker (native
4
+ * rewrite in reconcileIslandManifest + the React island-id onLoad plugin), and
5
+ * the bootstrap import all derive from this — so two same-named components from
6
+ * different files get distinct ids and never collide.
7
+ *
8
+ * Lives in its own module (no `scanImports` dep) so both `islands/build.ts` and
9
+ * `cli/native-routes-emit.ts` can import it without a circular dependency. */
10
+ export declare function islandChunkBasename(name: string, absSourcePath: string): string;
@@ -0,0 +1,2 @@
1
+ export declare const ISLANDS_IMPORTMAP_AND_BOOTSTRAP: string;
2
+ export declare const DIRECTIVES_BOOTSTRAP = "<script type=\"module\" src=\"/_brust/islands/_directives.js\" defer></script>";
@@ -0,0 +1,65 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ import type { IsrConfig } from './isr-jsx.ts';
3
+ /** Triggers that activate hydration of an island marker. */
4
+ export type HydrateTrigger = 'load' | 'idle' | 'visible' | 'interaction';
5
+ export interface IslandProps<P> {
6
+ /** Component rendered server-side INSIDE the marker. Same component
7
+ * the client chunk default-exports — SSR HTML must match the post-hydrate
8
+ * tree to avoid React reconciliation warnings. Its `Component.name` is the
9
+ * island id: it names the chunk (`<name>.js`) and the `data-brust-island`
10
+ * marker the client bootstrap reads, so it must be a stable, named
11
+ * component (no anonymous default export). */
12
+ component: ComponentType<P>;
13
+ /** Props passed to the component on both server and client. Must be
14
+ * JSON-serializable (no functions, classes, DOM nodes, etc.). Optional — a
15
+ * propless island (e.g. one that reads only global/client state) may omit it;
16
+ * it defaults to `{}`. On native routes, omitting `props` lowers to an empty
17
+ * props_path the renderer fills with `{}`. */
18
+ props?: P;
19
+ /** When to hydrate. Default 'load'. */
20
+ hydrate?: HydrateTrigger;
21
+ /** Native routes only: render this island server-side (renderToString during
22
+ * the loader crossing) so its markup ships in the HTML, then hydrate. Ignored
23
+ * on the React path (the whole tree already SSRs there). Default false. */
24
+ ssr?: boolean;
25
+ /**
26
+ * Optional ISR (incremental static regeneration) cache for an `ssr` island on
27
+ * a native-jinja route. When present, the island's `renderToString` runs ONCE
28
+ * per `key`; later requests serve the frozen markup from the Rust-side cache.
29
+ * Ignored unless `ssr` is set (caching a client-only island is meaningless).
30
+ *
31
+ * - `key` (required): unique string identifying this cache entry. A different
32
+ * key is a different cached render. Compute it in the loader and pass it
33
+ * through, e.g. `isr={{ key: data.cacheKey }}`.
34
+ * - `tags` (optional): groups for bulk invalidation —
35
+ * `import { cache } from 'brustjs'; cache.invalidate({ tags: ['blog'] })`
36
+ * evicts every entry carrying that tag. `cache.invalidate({ key })` evicts one.
37
+ * - `revalidate` (optional): TTL in **seconds** (integer). Omit to cache until
38
+ * explicitly invalidated.
39
+ *
40
+ * Example: `isr={{ key: data.cacheKey, tags: ['blog'], revalidate: 60 }}`
41
+ */
42
+ isr?: IsrConfig;
43
+ }
44
+ /** Per-render box tracking whether any `<Island>` rendered. Created fresh per
45
+ * render and provided through {@link IslandUsedContext}; the renderer reads
46
+ * `box.used` once at the end to decide whether to prepend the importmap +
47
+ * bootstrap script.
48
+ *
49
+ * This is REQUEST-SCOPED (not a module-scope flag) so concurrent renders in one
50
+ * isolate (renderSlots>1) never cross-contaminate: React restores each render's
51
+ * context stack across Suspense resumption, so an interleaved peer setting its
52
+ * own box never flips ours. A module `let` could not give that guarantee. */
53
+ export interface IslandUsedBox {
54
+ used: boolean;
55
+ }
56
+ /** Fresh per-render box. */
57
+ export declare function createIslandUsedBox(): IslandUsedBox;
58
+ /** Seed the Component→id registry (worker boot). */
59
+ export declare function configureIslandIdRegistry(entries: Iterable<[unknown, string]>): void;
60
+ /** Carries the per-render {@link IslandUsedBox} down to every `<Island>`. The
61
+ * renderer wraps the tree in a Provider with a fresh box; an `<Island>` rendered
62
+ * with no Provider (e.g. a standalone `renderToString` outside the React render
63
+ * path) reads `null` and is a no-op. */
64
+ export declare const IslandUsedContext: import("react").Context<IslandUsedBox>;
65
+ export declare function Island<P extends object>({ component: Component, props, hydrate, }: IslandProps<P>): ReactNode;
@@ -0,0 +1,31 @@
1
+ /** Shape of the `isr` attribute on an SSR component or `ssr` island on a
2
+ * native-jinja route. `renderToString` runs ONCE per `key`; later same-key
3
+ * requests serve the frozen markup from the Rust-side cache. */
4
+ export interface IsrConfig {
5
+ /** Required unique cache key. A different key is a different cached render.
6
+ * Compute it in the loader and pass it through (e.g. `data.cacheKey`). */
7
+ key: string;
8
+ /** Optional groups for bulk invalidation —
9
+ * `import { cache } from 'brustjs'; cache.invalidate({ tags: ['blog'] })`
10
+ * evicts every entry carrying a tag. `cache.invalidate({ key })` evicts one. */
11
+ tags?: string[];
12
+ /** Optional TTL in SECONDS (integer). Omit to cache until invalidated. */
13
+ revalidate?: number;
14
+ }
15
+ declare module 'react' {
16
+ namespace JSX {
17
+ interface IntrinsicAttributes {
18
+ /** brust ISR cache directive — compiler-consumed (stripped before the
19
+ * component is called), valid on any SSR component or `ssr` island on a
20
+ * native route. See {@link IsrConfig}. */
21
+ isr?: IsrConfig;
22
+ /** brust `native` inline directive — compiler-consumed (stripped at lower
23
+ * time). On a native-jinja route, `<Comp native />` expands the component
24
+ * inline at compile time (no JS-worker render) when it is pure
25
+ * (props→JSX, no hooks/side-effects); otherwise it degrades to a normal
26
+ * SSR component with a build warning. Bare boolean, like `ssr` on an
27
+ * island. */
28
+ native?: boolean;
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,89 @@
1
+ /** Set the directory the sidecar-manifest readers resolve against. Called once
2
+ * at boot from `index.ts`, mirroring `configureIslandsDir`/`configureCssDir`. */
3
+ export declare function configureJinjaDir(dir: string): void;
4
+ /** One entry of a `<template>.islands.json` manifest (enriched by T6). */
5
+ export interface NativeIslandEntry {
6
+ component: string;
7
+ instance: number;
8
+ propsPath: string;
9
+ /** Literal props value baked at build time (md pages). When present
10
+ * (`!== undefined` — `null` and falsy literals are valid), it is used as the
11
+ * props value and `propsPath` is ignored (md entries carry `propsPath: ''`
12
+ * since the field is required). */
13
+ propsLiteral?: unknown;
14
+ ssr: boolean;
15
+ hydrate: string;
16
+ sourcePath: string;
17
+ /** Dotted path into loader data yielding the ISR cache key (string). */
18
+ keyPath?: string;
19
+ /** Dotted path into loader data yielding the ISR cache tags (string[]). */
20
+ tagsPath?: string;
21
+ /** Revalidate window in SECONDS; converted to ttlMs on cache.set. */
22
+ revalidate?: number;
23
+ /** Static ISR cache key (string literal in JSX). Takes precedence over keyPath. */
24
+ keyLiteral?: string;
25
+ /** Static ISR cache tags (array literal in JSX). Takes precedence over tagsPath. */
26
+ tagsLiteral?: string[];
27
+ }
28
+ /** Rust-side ISR cache, injected as a port for testability. A `get` hit
29
+ * returns a FROZEN {html,props} pair (the props are the entity-encoded attr
30
+ * string, identical to what was stored) so the served markup and the hydrated
31
+ * props stay byte-identical regardless of live loader-data mutation. */
32
+ export interface IslandCache {
33
+ get(key: string): {
34
+ html: string;
35
+ props: string;
36
+ } | null;
37
+ set(key: string, tags: string[], ttlMs: number | undefined, html: string, props: string): void;
38
+ }
39
+ /** Walk a dotted path into `data`. Each segment must be an OWN enumerable
40
+ * property — inherited keys (`constructor`, `__proto__`, `toString`, …) yield
41
+ * `undefined` rather than traversing the prototype chain. This blocks both
42
+ * prototype-pollution-style reads AND the downstream crash where a resolved
43
+ * function (`Object`) makes `JSON.stringify` return `undefined`. A missing
44
+ * segment, a nullish/primitive cursor, or a non-own key all yield `undefined`.
45
+ * An empty path returns `data` itself. */
46
+ export declare function pathInto(data: unknown, propsPath: string): unknown;
47
+ /** HTML-entity-encode a string for a double-quoted attribute value. Order is
48
+ * load-bearing: `&` MUST be replaced first so the entities introduced by the
49
+ * later replacements aren't themselves double-encoded. Matches the compiler's
50
+ * `push_attr_escaped` charset (& < > ") so server-rendered markup and these
51
+ * props attrs stay consistent. */
52
+ export declare function entityEncode(s: string): string;
53
+ /** Read `<jinjaDir>/<templateName>.islands.json` and return the parsed entry
54
+ * array, or `null` if the file doesn't exist. `jinjaDir` defaults to the
55
+ * boot-configured jinja dir (`configureJinjaDir`), or `cwd/.brust/jinja` when
56
+ * unset; tests pass a temp dir. Both hits and misses are cached by absolute path. */
57
+ export declare function loadIslandManifest(templateName: string, jinjaDir?: string): NativeIslandEntry[] | null;
58
+ /** Build the per-island context additions for a manifest. Each entry
59
+ * contributes `island_<instance>_props` — the resolved props, JSON-stringified
60
+ * (undefined → null so it stays valid JSON) and entity-encoded. SSR entries
61
+ * (`ssr:true`) ALSO contribute `island_<instance>_html` — the island source
62
+ * component imported by absolute path and renderToString'd. The `instance` is a
63
+ * per-occurrence integer, so it's a safe key fragment. */
64
+ export declare function resolveIslandContext(manifest: NativeIslandEntry[], data: unknown, cache?: IslandCache): Promise<Record<string, string>>;
65
+ /** One entry in `<Name>.components.json` as enriched by `emitComponentArtifacts`. */
66
+ export interface NativeComponentEntry {
67
+ component: string;
68
+ instance: number;
69
+ sourcePath: string;
70
+ /** Dotted path into loader data yielding the ISR cache key (string). */
71
+ keyPath?: string;
72
+ /** Dotted path into loader data yielding the ISR cache tags (string[]). */
73
+ tagsPath?: string;
74
+ /** Revalidate window in SECONDS; converted to ttlMs on cache.set. */
75
+ revalidate?: number;
76
+ /** Static ISR cache key (string literal in JSX). Takes precedence over keyPath. */
77
+ keyLiteral?: string;
78
+ /** Static ISR cache tags (array literal in JSX). Takes precedence over tagsPath. */
79
+ tagsLiteral?: string[];
80
+ }
81
+ /** Read `<jinjaDir>/<templateName>.components.json` and return the parsed entry
82
+ * array, or `null` if the file doesn't exist. Both hits and misses are cached
83
+ * by absolute path (same invariant as `loadIslandManifest`). */
84
+ export declare function loadComponentManifest(templateName: string, jinjaDir?: string): NativeComponentEntry[] | null;
85
+ /** Build the per-component context additions for a manifest. Each entry
86
+ * contributes `comp_<instance>_html` — the component rendered to HTML by
87
+ * the route's factory function. On `renderToString` failure: degrade to
88
+ * `comp_N_html = ""` and log, mirrors SSR island failure behaviour. */
89
+ export declare function resolveComponentContext(manifest: NativeComponentEntry[], data: unknown, templateName: string, jinjaDir?: string, cache?: IslandCache): Promise<Record<string, string>>;
@@ -0,0 +1,18 @@
1
+ export declare function runInRequestCache<T>(fn: () => T): T;
2
+ /** Request-scoped memoize: share the in-flight promise + cache result for the
3
+ * scope's lifetime. Outside a scope → passthrough. Reject → guarded delete
4
+ * (identity-checked) so a stale catch can't evict a newer entry.
5
+ *
6
+ * NOTE: the reject cleanup runs one microtask after rejection, so a caller that
7
+ * dedupes the SAME key within that gap receives the about-to-reject promise (and
8
+ * thus the rejection) — acceptable: the result is one shared failure, not a hang. */
9
+ export declare function dedupe<T>(key: string, fn: () => Promise<T>): Promise<T>;
10
+ /** Idempotent (GET/HEAD) fetch deduped per request; non-idempotent → bypass.
11
+ * Returns a fresh clone every call (the stored Response is never exposed).
12
+ *
13
+ * NOTE: the cache key is `method + url` ONLY — it does NOT include `init`
14
+ * headers/body. Two `cachedFetch(sameUrl, {headers:…})` calls with DIFFERENT
15
+ * headers in one request share the FIRST call's response. Intended for plain
16
+ * idempotent GETs (the common loader case); if a caller varies `init` per call
17
+ * on the same URL, use `fetch` directly. */
18
+ export declare function cachedFetch(url: string, init?: RequestInit): Promise<Response>;
@@ -0,0 +1,14 @@
1
+ import type { FlatRoute } from '../routes.ts';
2
+ import type { McpManifest } from './manifest.ts';
3
+ export interface ExtractOptions {
4
+ /** Module exporting `defineActions(...)`. Convention `<scanRoot>/actions.ts`.
5
+ * Absent → zero tools (resources still extracted). */
6
+ actionsFile?: string;
7
+ /** The routes module file. */
8
+ routesFile: string;
9
+ /** User source roots. Reserved for future tsconfig resolution. */
10
+ sourceRoots: string[];
11
+ /** Result of `defineRoutes(...)`. */
12
+ routes: FlatRoute[];
13
+ }
14
+ export declare function extractMcpManifest(opts: ExtractOptions): Promise<McpManifest>;
@@ -0,0 +1,23 @@
1
+ import type { JsonSchema } from './schema.ts';
2
+ export interface ToolSchema {
3
+ name: string;
4
+ description?: string;
5
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
6
+ path: string;
7
+ inputSchema: JsonSchema;
8
+ outputSchema?: JsonSchema;
9
+ }
10
+ export interface ResourceSchema {
11
+ uriTemplate: string;
12
+ name: string;
13
+ description?: string;
14
+ outputSchema?: JsonSchema;
15
+ routeIndex: number;
16
+ }
17
+ export interface McpManifest {
18
+ version: 1;
19
+ tools: ToolSchema[];
20
+ resources: ResourceSchema[];
21
+ }
22
+ export declare function writeManifest(cwd: string, m: McpManifest): Promise<void>;
23
+ export declare function readManifest(cwd: string): Promise<McpManifest | null>;
@@ -0,0 +1,19 @@
1
+ import ts from 'typescript';
2
+ export interface JsonSchema {
3
+ type?: string | string[];
4
+ properties?: Record<string, JsonSchema>;
5
+ required?: string[];
6
+ items?: JsonSchema;
7
+ prefixItems?: JsonSchema[];
8
+ minItems?: number;
9
+ maxItems?: number;
10
+ additionalProperties?: JsonSchema | boolean;
11
+ anyOf?: JsonSchema[];
12
+ enum?: unknown[];
13
+ format?: string;
14
+ }
15
+ export interface ToJsonSchemaOptions {
16
+ unwrapPromise?: boolean;
17
+ checker?: ts.TypeChecker;
18
+ }
19
+ export declare function tsTypeToJsonSchema(type: ts.Type, opts?: ToJsonSchemaOptions): JsonSchema | undefined;
@@ -0,0 +1,15 @@
1
+ import type { FlatRoute, BrustRequest } from '../routes.ts';
2
+ import type { EndpointDef } from '../define-actions.ts';
3
+ import type { McpManifest } from './manifest.ts';
4
+ export interface McpServerOptions {
5
+ manifest: McpManifest;
6
+ endpoints: EndpointDef[];
7
+ routes: FlatRoute[];
8
+ packageVersion?: string;
9
+ }
10
+ export interface McpServer {
11
+ handleRequest(jsonRpcBody: string, req: BrustRequest): Promise<string>;
12
+ }
13
+ export declare function makeMcpServer(opts: McpServerOptions): McpServer;
14
+ export declare function makeResult(id: number | string | null, result: unknown): string;
15
+ export declare function makeError(id: number | string | null, code: number, message: string): string;