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,8 @@
1
+ /** Browser dev client. Inlined into the SSR first chunk via a <script
2
+ * type="module">…</script> wrapper. Connects WS at /_brust/dev, handles
3
+ * reload / css-update / error / ok messages, manages a red overlay.
4
+ *
5
+ * Keep this string short — it ships in every dev-mode SSR response. */
6
+ export declare const DEV_CLIENT_JS: string;
7
+ /** Build the full <script> tag that gets spliced before </head>. */
8
+ export declare function buildDevClientTag(): string;
@@ -0,0 +1,33 @@
1
+ import type { DevMessage } from './ws-channel.ts';
2
+ import type { ChangeKind } from './watcher.ts';
3
+ export interface CoordinatorDeps {
4
+ workers: {
5
+ terminateAll(): Promise<void>;
6
+ spawnAll(): Promise<void>;
7
+ };
8
+ buildCss: () => Promise<void>;
9
+ buildIslands: () => Promise<void>;
10
+ /** Recompile native-route `.jinja` templates from source and reload them into
11
+ * the minijinja env, so `native: true` routes pick up .tsx edits on reload. */
12
+ reEmitJinja: () => Promise<void>;
13
+ /** Clear the Rust-side island ISR cache. Called on every render-affecting
14
+ * reload (`ts`/`html`/`islands`) so a `.tsx` edit is reflected immediately —
15
+ * a frozen island render from before the edit must never survive a hot reload.
16
+ * Optional: a build without the island-cache addon export omits it. */
17
+ clearIslandCache?: () => void;
18
+ buildComponentCss?: () => Promise<void>;
19
+ snapshotComponentCss?: () => Promise<import('../css/manifest.ts').ComponentCssManifest | null>;
20
+ broadcast: (msg: DevMessage) => Promise<void> | void;
21
+ tui: {
22
+ appendEvent(line: string): void;
23
+ };
24
+ }
25
+ export declare class Coordinator {
26
+ private deps;
27
+ private state;
28
+ constructor(deps: CoordinatorDeps);
29
+ handleChange(ev: {
30
+ paths: string[];
31
+ kind: ChangeKind;
32
+ }): Promise<void>;
33
+ }
@@ -0,0 +1,6 @@
1
+ /** Set the dev-client snippet (full `<script>…</script>` tag) the renderer
2
+ * should inject before `</head>`. Pass `null` to disable injection (the
3
+ * default in non-dev mode). */
4
+ export declare function configureDevClientSnippet(s: string | null): void;
5
+ /** Returns the configured snippet, or `null` when dev mode is off. */
6
+ export declare function getDevClientSnippet(): string | null;
@@ -0,0 +1,7 @@
1
+ /** Called once by `brust dev` after the initial native-template emit. */
2
+ export declare function registerJinjaReEmit(fn: () => Promise<void>): void;
3
+ /** Called by the dev coordinator on a ts/html/islands hot reload. No-op when no
4
+ * callback is registered (e.g. unit tests, or an app with no native routes). */
5
+ export declare function reEmitJinja(): Promise<void>;
6
+ /** Test helper. */
7
+ export declare function _resetForTests(): void;
@@ -0,0 +1,35 @@
1
+ interface Status {
2
+ port: number;
3
+ workers: number;
4
+ watching: string[];
5
+ }
6
+ export interface TuiOptions {
7
+ isTty: boolean;
8
+ write: (s: string) => void;
9
+ capacity?: number;
10
+ }
11
+ /** Astro-style dev output: a clean one-time banner on ready, then event lines
12
+ * that scroll naturally below it. Deliberately NOT a full-screen TUI — it never
13
+ * hides the cursor or clears the screen, so the terminal is left exactly as it
14
+ * was found when the dev server exits (no more vanished cursor on Ctrl-C). */
15
+ export declare class Tui {
16
+ private opts;
17
+ private events;
18
+ private capacity;
19
+ private bannerShown;
20
+ constructor(opts: TuiOptions);
21
+ eventsForTests(): string[];
22
+ /** Print the ready banner. Idempotent — only the first call emits it. */
23
+ updateStatus(s: Status): void;
24
+ /** Append one event. Scrolls below the banner (TTY) or prints a plain line
25
+ * (non-TTY / CI). The in-memory ring buffer backs `eventsForTests`. */
26
+ appendEvent(line: string): void;
27
+ /** Called on shutdown. There is nothing to restore (we never altered the
28
+ * terminal mode); the reset is belt-and-braces so a half-written colored
29
+ * line can't bleed into the user's prompt. */
30
+ stop(): void;
31
+ /** Colorize an event line in TTY mode: a dim timestamp prefix, with ok/error
32
+ * markers tinted. The coordinator emits ` → ok (Nms)` and ` ✗ <msg>`. */
33
+ private format;
34
+ }
35
+ export {};
@@ -0,0 +1,34 @@
1
+ export type ChangeKind = 'ts' | 'css' | 'component-css' | 'html' | 'islands' | 'md';
2
+ /** Classify a changed path. Returns null when the path should be ignored.
3
+ * `root` is used to compute the relative path for ignore-segment matching.
4
+ * `hasMdRoutes` gates the `'md'` kind (S4): when the app has no md routes, a
5
+ * project `.md` edit (README.md, notes) must not trigger the full reload path
6
+ * (island rebuild + worker restart) — it classifies as null instead. Defaults
7
+ * to true for backward compatibility with callers that don't thread the flag. */
8
+ export declare function classifyPath(absPath: string, root: string, hasMdRoutes?: boolean): ChangeKind | null;
9
+ interface Coalesce {
10
+ add(path: string): void;
11
+ flush(): void;
12
+ }
13
+ /** Internal — exposed for unit tests. */
14
+ export declare function _testCoalesce(debounceMs: number, flush: (paths: string[]) => void): Coalesce;
15
+ export interface CreateWatcherOptions {
16
+ root: string;
17
+ debounceMs?: number;
18
+ /** Whether the app has md routes — gates `.md` classification (see
19
+ * classifyPath). Defaults to true (back-compat). */
20
+ hasMdRoutes?: boolean;
21
+ onChange: (ev: {
22
+ paths: string[];
23
+ kind: ChangeKind;
24
+ }) => void;
25
+ }
26
+ export interface Watcher {
27
+ close(): void;
28
+ }
29
+ /** Watch `root` recursively. Emits one `onChange` call per debounce window
30
+ * with paths classified by the dominant kind. Mixed-kind windows pick
31
+ * by priority: islands > ts > html > css (islands trigger a full restart
32
+ * that subsumes the others). */
33
+ export declare function createWatcher(opts: CreateWatcherOptions): Watcher;
34
+ export {};
@@ -0,0 +1,17 @@
1
+ /** Called once by brust.serve() in dev mode AFTER it spawns the initial
2
+ * pool. Hands the references to the registry so the coordinator can
3
+ * churn them later. */
4
+ export declare function registerInitialPool(workers: Worker[], entry: string, count: number, baseEnv: Record<string, string>): void;
5
+ /** Terminate every Worker with a 2s per-worker grace. If termination
6
+ * doesn't return in time, abandon the reference and continue. */
7
+ export declare function terminateAll(): Promise<void>;
8
+ /** Spawn `count` fresh Workers using the entry + env captured at
9
+ * registerInitialPool time. Each worker gets BRUST_WORKER_ID=i.
10
+ * Resolves only after every fresh worker reports `brust-worker-ready`
11
+ * via postMessage — so the caller (coordinator) doesn't broadcast
12
+ * `reload` against workers whose message listeners aren't installed
13
+ * yet. Falls back after a 5s grace if a worker never signals. */
14
+ export declare function spawnAll(): Promise<void>;
15
+ /** Test helper. */
16
+ export declare function _workersForTests(): Worker[];
17
+ export declare function _resetForTests(): void;
@@ -0,0 +1,39 @@
1
+ import type { Route } from '../routes.ts';
2
+ /** Server-to-client protocol. Client never sends after open. */
3
+ export type DevMessage = {
4
+ type: 'building';
5
+ } | {
6
+ type: 'reload';
7
+ } | {
8
+ type: 'css-update';
9
+ href: string;
10
+ } | {
11
+ type: 'error';
12
+ message: string;
13
+ stack?: string;
14
+ } | {
15
+ type: 'ok';
16
+ };
17
+ export declare function _clientCountForTests(): number;
18
+ export declare function _resetForTests(): void;
19
+ /** Build the synthetic /_brust/dev WS route. brust.run() prepends this
20
+ * to opts.routes in dev mode (both main + worker route arrays). */
21
+ export declare function createDevWsRoute(): Route;
22
+ /** Worker-side: install a message listener that receives `dev-broadcast`
23
+ * envelopes from the main process and forwards them to this worker's
24
+ * local clients. Idempotent. After install, posts `brust-worker-ready`
25
+ * back to the parent so spawnAll() knows the worker is ready to relay
26
+ * broadcasts (avoids a race where `reload` is dispatched before the
27
+ * fresh worker's listener is wired). */
28
+ export declare function installWorkerBroadcastListener(): void;
29
+ /** Main-side: push the message to every `/_brust/dev` client through the napi
30
+ * addon. The dev WS is a Rust-owned control channel (see the `/_brust/dev`
31
+ * branch in crates/brust/src/server.rs), so the frame is delivered straight
32
+ * through each connection's Rust-owned send_tx and SURVIVES a hot reload's
33
+ * worker restart.
34
+ *
35
+ * The previous implementation relayed main→worker→worker-local-clients, which
36
+ * lost the `reload` frame: the coordinator broadcasts it AFTER terminateAll(),
37
+ * by which point the connection's worker-side registration had died with the
38
+ * old worker. */
39
+ export declare function broadcast(msg: DevMessage): Promise<void>;
@@ -0,0 +1,23 @@
1
+ export interface GeneratorStrings {
2
+ /** Full meta tag, e.g. `<meta name="generator" content="brust 0.1.48-alpha"/>` */
3
+ meta: string;
4
+ /** X-Powered-By value, e.g. `brust/0.1.48-alpha` */
5
+ header: string;
6
+ }
7
+ /** Build the resolved strings. Version comes from the brustjs package.json
8
+ * (readVersion never throws — "unknown" degrades to name-only, never a crash).
9
+ * The version is sanitized to attr/header-safe bytes; semver chars only. */
10
+ export declare function generatorStrings(versionOn: boolean): GeneratorStrings;
11
+ /** Insert the generator meta immediately after the compiler-emitted viewport
12
+ * meta. Anchor missing (non-document template) → no-op, never an error.
13
+ * CALLER CONTRACT: pass fresh compiler output — this function does not check
14
+ * for an existing tag, so calling it twice on the same string duplicates.
15
+ * Every emit path recompiles from source each run, which keeps this safe. */
16
+ export declare function insertGeneratorMeta(jinja: string, metaTag: string): string;
17
+ /** Write the decision artifact into `dir` (a jinja out dir), creating it. */
18
+ export declare function writeGeneratorArtifact(dir: string, strings: GeneratorStrings): void;
19
+ /** Read the artifact; null on missing/malformed (caller decides the fallback). */
20
+ export declare function readGeneratorArtifact(dir: string): GeneratorStrings | null;
21
+ /** Artifact if present, else version-on defaults — the spec's fallback policy
22
+ * (an old dist with no artifact behaves as default = version on). */
23
+ export declare function resolveGenerator(dir: string): GeneratorStrings;
@@ -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 | null>;
65
+ export declare function Island<P extends object>({ component: Component, props, hydrate, }: IslandProps<P>): ReactNode;