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,72 @@
1
+ import { type MdRouteSource } from './routes.ts';
2
+ /** The slice of a FlatRoute the md emit step reads. The chain holds the route
3
+ * NODES, so the md leaf's `__mdSource` (runtime/md/routes.ts) survives into it.
4
+ * `fullPath` is only read by the manifest derivation (emitMdArtifacts).
5
+ * `Component` admits plain `{ name }` carriers AND real component values
6
+ * (function/class — both expose `.name` via the Function prototype), so a
7
+ * `FlatRoute[]` passes without casts. */
8
+ export interface FlatRouteLike {
9
+ fullPath?: string;
10
+ nativeTemplate?: string;
11
+ chain?: Array<{
12
+ Component?: {
13
+ name?: string;
14
+ } | ((...args: never[]) => unknown) | (abstract new (...args: never[]) => unknown);
15
+ __mdSource?: MdRouteSource;
16
+ }>;
17
+ }
18
+ export interface MdEmitOpts {
19
+ /** User's routes entry file (absolute path) — scanned for the default-import
20
+ * idents that resolve embedded component tags, and for app-wide directives. */
21
+ entryFile: string;
22
+ /** Flat routes; only chains whose LEAF carries `__mdSource` are emitted. */
23
+ flatRoutes: FlatRouteLike[];
24
+ /** Jinja output dir (same dir `emitNativeTemplates` writes to). */
25
+ outDir: string;
26
+ /** Bake the /_brust/dev WS client tag (parity with the native emit's
27
+ * BRUST_DEV injection — md pages render Rust-side and never pass through the
28
+ * React renderer's dev-client injection). */
29
+ withDevClient?: boolean;
30
+ /** What to do when a route's md file no longer exists on disk (deleted after
31
+ * the route table was built). emitMdTemplates serves BOTH `brust build` and
32
+ * the dev re-emit, and the two must diverge here:
33
+ * - `'throw'` (build): a missing file is a HARD error — silently skipping
34
+ * ships a dist with the route registered but its template absent.
35
+ * - `'skip-warn'` (default; dev boot/re-emit): skip the route and warn
36
+ * once that a restart is required — the dev route table is frozen at
37
+ * boot, so a crash here would take down the whole hot-reload loop. */
38
+ onMissing?: 'throw' | 'skip-warn';
39
+ }
40
+ /** Test helper. */
41
+ export declare function _resetMdRoutesChangedWarnForTests(): void;
42
+ /** Emit one `.jinja` (+ `.islands.json` sidecar) per md route in `flatRoutes`.
43
+ * Returns the islands referenced from md content (`name → absolute source
44
+ * path`) so the build step can thread them into island-chunk discovery as
45
+ * `extraIslands` (task 2.8). Behaviors are NOT in the map — their chunks are
46
+ * built by `scanDirectiveComponents`, which already walks the routes-entry
47
+ * import graph the registry keeps alive. */
48
+ export declare function emitMdTemplates(opts: MdEmitOpts): Promise<{
49
+ mdIslands: Map<string, string>;
50
+ }>;
51
+ export interface MdArtifactsOpts extends MdEmitOpts {
52
+ /** Dirs to write `md-manifest.json` into when the app has md routes (e.g.
53
+ * `<distDir>` and `<cwd>/.brust` — both "next to" their jinja dirs, where
54
+ * `loadPrebuiltMdManifest` / the staleness check look). Skipped entirely
55
+ * when there are no md routes (zero output difference for md-free apps). */
56
+ manifestDirs?: string[];
57
+ }
58
+ /** Task 2.8 build-integration seam: emit the md `.jinja` templates AND the
59
+ * frozen `md-manifest.json` (derived from the same flat route table — single
60
+ * source of truth) in one call. All build sites (`brust build`, `brust dev`
61
+ * boot/re-emit, runtime boot, dev HMR island rebuild) go through this so the
62
+ * template, manifest, and returned `mdIslands` chunk inputs can never diverge.
63
+ * Strict no-op for apps without md routes. */
64
+ export declare function emitMdArtifacts(opts: MdArtifactsOpts): Promise<{
65
+ mdIslands: Map<string, string>;
66
+ }>;
67
+ /** Replace the slot element's inner content with the md HTML. The slot attr
68
+ * itself stays (hydration-neutral, useful for tests). Exactly one slot must
69
+ * exist and it must be empty — both are emit-pipeline invariants, so a
70
+ * violation is a hard error, not a soft skip. `name` is a generated template
71
+ * name (`[A-Za-z0-9_]` only), so interpolating it into the regex is safe. */
72
+ export declare function spliceMdSlot(template: string, name: string, mdHtml: string): string;
@@ -0,0 +1,80 @@
1
+ export type MdHydrate = 'load' | 'idle' | 'visible' | 'interaction';
2
+ /** One embedded-component use found in a markdown page (islands only). */
3
+ export interface MdIslandUse {
4
+ name: string;
5
+ /** 0-based per page; the emit step offsets past the wrapper's own islands. */
6
+ instanceLocal: number;
7
+ props: Record<string, unknown>;
8
+ hydrate: MdHydrate;
9
+ csr: boolean;
10
+ /** 1-based line number within the md body (post-frontmatter). */
11
+ line: number;
12
+ }
13
+ /** One behavior-component (x-data) use found in a markdown page. */
14
+ export interface MdBehaviorUse {
15
+ name: string;
16
+ directive: string;
17
+ /** 1-based line number within the md body (post-frontmatter). */
18
+ line: number;
19
+ /** Literal tag props (string/number only — validated at extract time). The
20
+ * emit step inline-substitutes them into the component's compiled body. */
21
+ props: Record<string, unknown>;
22
+ /** The EXACT placeholder host markup injected into the rendered HTML. The
23
+ * emit step (which owns compileJsx) substitutes the fully inlined component
24
+ * body over this exact string. It carries the per-render nonce, so user
25
+ * prose can never collide with it. */
26
+ marker: string;
27
+ }
28
+ export type MdComponentResolution = {
29
+ kind: 'island';
30
+ id: string;
31
+ } | {
32
+ kind: 'behavior';
33
+ directive: string;
34
+ };
35
+ export interface RenderMdPageOptions {
36
+ /** Markdown source, frontmatter already stripped. */
37
+ body: string;
38
+ absPath: string;
39
+ /** `null` → unknown name → renderMdPage throws with `file:line`. */
40
+ resolve: (name: string, line: number) => MdComponentResolution | null;
41
+ }
42
+ interface ShikiLike {
43
+ codeToHtml(code: string, options: {
44
+ lang: string;
45
+ themes: {
46
+ light: string;
47
+ dark: string;
48
+ };
49
+ }): Promise<string>;
50
+ }
51
+ /** Test seam: replaces the dynamic `import('shiki')` and resets all cached state. */
52
+ export declare function __setShikiImporterForTests(importer: (() => Promise<ShikiLike>) | null): void;
53
+ /**
54
+ * Highlights a code fence via shiki (lazy-imported once, cached) with dual
55
+ * CSS-variables themes. shiki absent → escape-only `<pre><code>` fallback and
56
+ * ONE warning per build. Unknown language → silent escape-only fallback.
57
+ */
58
+ export declare function highlightCode(code: string, lang: string): Promise<string>;
59
+ /**
60
+ * Replaces every minijinja delimiter in md-origin HTML with a string-literal
61
+ * expression that renders back to the original text. Single pass — the
62
+ * replacements themselves are never re-matched. Component-host markup is
63
+ * injected AFTER this pass so its jinja stays live.
64
+ */
65
+ export declare function neutralizeBraces(html: string): string;
66
+ /**
67
+ * Renders one markdown page body to jinja-safe HTML.
68
+ *
69
+ * Order is load-bearing (locked by tests):
70
+ * 1. extract component-tag lines to opaque placeholders (marked never sees them)
71
+ * 2. render markdown (GFM, heading ids, shiki fences)
72
+ * 3. neutralize jinja braces over the rendered HTML
73
+ * 4. substitute placeholders with host markup — its jinja stays live
74
+ */
75
+ export declare function renderMdPage(opts: RenderMdPageOptions): Promise<{
76
+ html: string;
77
+ islands: MdIslandUse[];
78
+ behaviors: MdBehaviorUse[];
79
+ }>;
80
+ export {};
@@ -0,0 +1,119 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { Route } from '../routes.ts';
3
+ import { type MdFile } from './scan.ts';
4
+ /**
5
+ * Content-addressed jinja template name for an md page:
6
+ * `Md_<sanitized relPath>_<8hex(sha256 relPath)>`.
7
+ *
8
+ * Sanitizes `[^A-Za-z0-9_]` to `_`; the hash (same scheme as
9
+ * `islandChunkBasename` in runtime/islands/chunk-id.ts) keeps two relPaths
10
+ * that sanitize identically (e.g. `a-b.md` vs `a_b.md`) from colliding.
11
+ */
12
+ export declare function mdTemplateName(relPath: string): string;
13
+ /**
14
+ * Maps a content-relative md path to its URL under `prefix`.
15
+ * `index.md` maps to the prefix itself; `guide/index.md` maps to
16
+ * `<prefix>/guide`; `query/where.md` maps to `<prefix>/query/where`.
17
+ * Trailing slashes on `prefix` are normalized away (`/docs/` == `/docs`).
18
+ */
19
+ export declare function mdUrlPath(relPath: string, prefix: string): string;
20
+ /** Build-time source info attached to every md leaf route. Plain field on the
21
+ * Route node, so it survives `flattenRoutes` into `FlatRoute.chain` (the chain
22
+ * holds the node objects); the emit step filters chains whose leaf has it. */
23
+ export interface MdRouteSource {
24
+ absPath: string;
25
+ relPath: string;
26
+ contentDir: string;
27
+ frontmatter: MdFile['frontmatter'];
28
+ components: Record<string, ComponentType<any>>;
29
+ layoutName?: string;
30
+ }
31
+ /** An md leaf route — an ordinary native Route plus the `__mdSource` marker. */
32
+ export type MdRoute = Route & {
33
+ __mdSource: MdRouteSource;
34
+ };
35
+ export interface MdRoutesOptions {
36
+ /** URL prefix the pages mount under. Default `'/'`. */
37
+ prefix?: string;
38
+ /** Optional layout component — when set, mdRoutes returns ONE parent route
39
+ * `{ path: prefix, Component: layout, children: [...mdLeaves] }`. */
40
+ layout?: ComponentType<any>;
41
+ /** Component-tag registry for `<Name />` tags inside the md body. */
42
+ components?: Record<string, ComponentType<any>>;
43
+ /** Loader for the LAYOUT parent route — runs before each md leaf renders and
44
+ * its data merges into the leaf's context top-down (so it feeds the layout
45
+ * chrome: sidebar, pager, …). Only applies when `layout` is set. Return keys
46
+ * that DON'T collide with the leaf's `__md` head metadata, which must win.
47
+ * Without this, the layout loader had to be attached by mutating the returned
48
+ * route node (`tree.loader = …`) — undocumented surface (was FRAMEWORK-GAPS G2). */
49
+ loader?: Route['loader'];
50
+ }
51
+ /** One frozen-manifest entry (everything route construction needs per page).
52
+ * `contentDir` is only present when the entry belongs to a DIFFERENT content
53
+ * dir than the manifest's top-level `contentDir` (apps mounting two or more
54
+ * `mdRoutes()` dirs share ONE manifest file per dist) — additive, so version-1
55
+ * manifests without it stay readable. */
56
+ export interface MdManifestEntry {
57
+ relPath: string;
58
+ templateName: string;
59
+ urlPath: string;
60
+ frontmatter: MdFile['frontmatter'];
61
+ contentDir?: string;
62
+ }
63
+ export interface MdManifest {
64
+ version: 1;
65
+ contentDir: string;
66
+ entries: MdManifestEntry[];
67
+ }
68
+ export declare const MD_MANIFEST_FILENAME = "md-manifest.json";
69
+ /** Write the frozen md manifest as `<dir>/md-manifest.json` (creates `dir`).
70
+ * Returns the absolute file path written. */
71
+ export declare function writeMdManifest(dir: string, entries: MdManifestEntry[], contentDir: string): string;
72
+ /** Read + schema-check a frozen md manifest. Throws on a missing file, bad
73
+ * JSON, or an unsupported version (fail loudly — the manifest is build output). */
74
+ export declare function readMdManifest(file: string): MdManifest;
75
+ /** The slice of a FlatRoute the manifest derivation reads (structural — avoids
76
+ * a circular import with runtime/md/emit.ts's FlatRouteLike). `Component` is
77
+ * declared (as unknown — never read here) so the all-optional shape shares a
78
+ * property with Route and FlatRoute[] passes TS weak-type detection. */
79
+ export interface MdManifestFlatRouteLike {
80
+ fullPath?: string;
81
+ nativeTemplate?: string;
82
+ chain?: Array<{
83
+ Component?: unknown;
84
+ __mdSource?: MdRouteSource;
85
+ }>;
86
+ }
87
+ /** Derive the frozen md manifest from the FLAT ROUTE TABLE (single source of
88
+ * truth — never re-scan the fs at manifest-write time). Returns `null` when the
89
+ * app has no md routes, so callers can skip ALL md output (the `brust build`
90
+ * byte-identical invariant for md-free apps). With multiple `mdRoutes()` dirs,
91
+ * the first dir seen is the manifest's top-level `contentDir`; entries from
92
+ * other dirs carry a per-entry `contentDir`. */
93
+ export declare function mdManifestFromFlatRoutes(flatRoutes: MdManifestFlatRouteLike[]): {
94
+ contentDir: string;
95
+ entries: MdManifestEntry[];
96
+ } | null;
97
+ /** Turn a directory of `.md` files into native Route entries. Each file gets a
98
+ * synthetic named component (name = its jinja template name, satisfying
99
+ * `validateRoute`'s native checks) and a loader exposing the frontmatter as
100
+ * `{ __md: { title, description } }`. With `layout`, returns ONE parent route
101
+ * at `prefix` whose children carry prefix-relative paths; without, the leaves
102
+ * carry the full prefixed path. */
103
+ export declare function mdRoutes(contentDir: string, opts?: MdRoutesOptions): Route[];
104
+ export interface MdNavItem {
105
+ title: string;
106
+ path: string;
107
+ order?: number;
108
+ }
109
+ export interface MdNavGroup {
110
+ group: string | null;
111
+ items: MdNavItem[];
112
+ }
113
+ /** Navigation model for a content dir: items grouped by `frontmatter.nav.group`
114
+ * (ungrouped pages land in a `group: null` top-level bucket), sorted by
115
+ * `nav.order` (missing order sorts last) then title. Paths use the prefix the
116
+ * dir was mounted under by `mdRoutes` (frozen-manifest urlPaths in a prebuilt
117
+ * run; `'/'` if mdRoutes was never called for the dir). Group order follows
118
+ * the first appearance of each group in the sorted item sequence. */
119
+ export declare function mdNav(contentDir: string): MdNavGroup[];
@@ -0,0 +1,34 @@
1
+ export interface MdFile {
2
+ /** Posix-separated path relative to the content dir, e.g. 'query/where.md'. */
3
+ relPath: string;
4
+ absPath: string;
5
+ frontmatter: {
6
+ title?: string;
7
+ description?: string;
8
+ nav?: {
9
+ group?: string;
10
+ order?: number;
11
+ };
12
+ [k: string]: unknown;
13
+ };
14
+ /** Markdown source after the frontmatter block is stripped. */
15
+ body: string;
16
+ }
17
+ /**
18
+ * Recursively scans `contentDir` for `.md` files, sorted by `relPath`.
19
+ *
20
+ * Frontmatter is a leading `---` … `---` block parsed as a hand-rolled YAML
21
+ * subset (NO yaml dependency):
22
+ * - `key: value` lines; keys match `[A-Za-z0-9_-]+`
23
+ * - values: double-quoted strings (JSON escapes), single-quoted strings (no
24
+ * escapes), bare strings, numbers, booleans
25
+ * - one-level nested maps via the INLINE-BRACES form only, e.g.
26
+ * `nav: { group: "Getting Started", order: 1 }` — indented child keys are
27
+ * NOT supported and throw
28
+ * - blank lines inside the block are ignored
29
+ *
30
+ * Files without a frontmatter block get `frontmatter: {}` and the whole file
31
+ * as `body`. Malformed frontmatter throws with `<absPath>:<line>`. CRLF line
32
+ * endings are tolerated.
33
+ */
34
+ export declare function scanMdDir(contentDir: string): MdFile[];
@@ -0,0 +1 @@
1
+ export declare function slugifyHeading(text: string): string;
@@ -0,0 +1,30 @@
1
+ /** The single island-vs-behavior classifier: a component source is a native
2
+ * behavior component iff it has `export const behavior`. Shared by
3
+ * `scanDirectiveComponents` and the md emit step (runtime/md/emit.ts) so the
4
+ * two paths can never diverge. */
5
+ export declare function isBehaviorSource(src: string): boolean;
6
+ /** Deterministic, app-unique directive name = camelCase(basename) + "_" + 8 hex of
7
+ * sha256(cwd-relative path). The SINGLE name contract: chunk filename, runtime
8
+ * registry key, and the compiler-emitted `x-data` all derive from this. */
9
+ export declare function directiveName(absPath: string, projectRoot: string): string;
10
+ /** BFS the local import graph from the routes entry; return directiveName →
11
+ * absolute sourcePath for every file that has `export const behavior`. Throws on
12
+ * two distinct files deriving the same directive name (path-hashed, collision-resistant). */
13
+ export declare function scanDirectiveComponents(routesEntryFile: string): Map<string, string>;
14
+ export interface BuildDirectivesResult {
15
+ outDir: string;
16
+ count: number;
17
+ /** All emitted filenames (basenames): `_directives.js` (the shared runtime) plus
18
+ * one `<name>.directive.js` per component. Callers mirror these into .brust/islands. */
19
+ files: string[];
20
+ }
21
+ /** Build the shared directive runtime (`_directives.js`, loaded on every native page)
22
+ * PLUS one `<name>.directive.js` chunk per component (loaded ON DEMAND by the runtime
23
+ * when an `x-data="<name>"` appears). Splitting behaviors into per-component chunks
24
+ * keeps a page from downloading the JS of components it never renders; the runtime
25
+ * dynamically `import()`s a chunk the first time its name is seen (initial or post
26
+ * SPA-nav). Each chunk self-registers via the global `register` handle (so it needs
27
+ * no runtime import). React must not leak into any chunk — asserted per file. */
28
+ export declare function buildDirectives(components: Map<string, string>, options: {
29
+ outDir: string;
30
+ }): Promise<BuildDirectivesResult>;
@@ -0,0 +1,2 @@
1
+ export { register, start } from './runtime.ts';
2
+ export type { Behavior, BehaviorCtx, Instance } from './runtime.ts';
@@ -0,0 +1,52 @@
1
+ export type Instance = Record<string, unknown>;
2
+ /** What a `behavior` receives. Beyond `el`/`props`, two lifecycle helpers whose
3
+ * teardown auto-joins the component's disposer set (run on unmount / SPA-nav swap):
4
+ * - `effect(fn)` — a reactive effect (React `useEffect` semantics: `fn` may return
5
+ * a cleanup that runs before each re-run and on unmount). Use it
6
+ * for side-effects on signal change (sync localStorage, the DOM
7
+ * outside the component, timers). Returns the disposer too.
8
+ * - `onCleanup(fn)` — register a one-shot teardown for unmount (e.g. removeEventListener). */
9
+ export interface BehaviorCtx {
10
+ el: HTMLElement;
11
+ props: unknown;
12
+ effect: (fn: () => void | (() => void)) => () => void;
13
+ onCleanup: (fn: () => void) => void;
14
+ }
15
+ export type Behavior = (ctx: BehaviorCtx) => Instance;
16
+ /** Register a component behavior under `name`. Called by `<name>.directive.js` chunks
17
+ * via the global handle below (they do NOT import this module — keeps each chunk to
18
+ * just its behavior, with the runtime shared as the single `_directives.js` copy). */
19
+ export declare function register(name: string, behavior: Behavior): void;
20
+ /** Scan `root` (default: document) for [x-data], mount each, and (once) attach a
21
+ * MutationObserver for dynamic mount/dispose. Idempotent. NOTE: `root` scopes the
22
+ * INITIAL scan only; the observer always watches the global `document.body` (one
23
+ * observer per document handles every later mount/dispose, incl. SPA-nav swaps),
24
+ * plus one observer per discovered OPEN shadow root (a body observer cannot see
25
+ * mutations inside a shadow tree). */
26
+ export declare function start(root?: ParentNode): void;
27
+ export interface ForExpr {
28
+ itemName: string;
29
+ indexName?: string;
30
+ listPath: string;
31
+ keyPaths?: string[];
32
+ }
33
+ /** Parse an x-for expression. Grammar:
34
+ * (item[, index]) in listPath [by keyPath, keyPath, ...]
35
+ * Returns null on malformed input (caller warns + skips). */
36
+ export declare function parseFor(raw: string): ForExpr | null;
37
+ /** Write `value` into the signal at `path`, unwrapping intermediate signal/computed
38
+ * hops like `read()` (resolveRaw does NOT unwrap intermediates and cannot be the base
39
+ * for multi-hop paths). The LEAF is never called: `isSignal(leaf)` → `.set(value)`,
40
+ * else warn once and skip. */
41
+ export declare function writePath(scope: Instance, path: string, value: unknown): void;
42
+ /** Apply a bound value to a DOM attr/property. class → className; boolean props →
43
+ * property (when present) + attribute presence; value → property; else attribute. */
44
+ export declare function setBound(el: HTMLElement, attr: string, value: unknown): void;
45
+ /** Walk a dotted member path against `scope`, unwrapping a signal/computed at EVERY
46
+ * hop (so an intermediate item-signal is tracked by `effect`); at the LEAF also call
47
+ * a plain function to obtain its value (this read is what `effect` tracks). */
48
+ export declare function read(scope: Instance, path: string): unknown;
49
+ /** Resolve a dotted path WITHOUT calling the leaf (for x-on handlers). */
50
+ export declare function resolveRaw(scope: Instance, path: string): unknown;
51
+ /** Resolve `path` on `scope` and, if a function, call it with the event. */
52
+ export declare function callMethod(scope: Instance, path: string, event: Event): void;
@@ -0,0 +1,2 @@
1
+ export declare function installActiveNav(): void;
2
+ export declare function __resetActiveNavForTest(): void;
@@ -0,0 +1,5 @@
1
+ export type { NavPhase, NavState, NavStore } from './store.ts';
2
+ export { nav, getNavState, subscribe, onBeforeNavigate, onNavigate, onNavigateError, } from './store.ts';
3
+ export { installActiveNav } from './active-nav.ts';
4
+ export { navigate, buildSearch } from './navigate.ts';
5
+ export type { NavigateOptions, QueryInit, QueryValue } from './navigate.ts';
@@ -0,0 +1,14 @@
1
+ export type QueryValue = string | number | boolean;
2
+ export type QueryInit = Record<string, QueryValue | null | undefined | ReadonlyArray<QueryValue>>;
3
+ export interface NavigateOptions {
4
+ query?: QueryInit;
5
+ replace?: boolean;
6
+ }
7
+ /** Serialize a query object to a `?...` string (or '' when empty). Pure, DOM-free. */
8
+ export declare function buildSearch(query: QueryInit): string;
9
+ /** Programmatic SPA navigation. Resolves `path` against the current location,
10
+ * merges `options.query` over any query already in `path` (each key replaces all
11
+ * base occurrences; null/undefined deletes), then delegates to the registered
12
+ * navigator (full SPA swap + nav-state lifecycle). No navigator registered (no
13
+ * islands bootstrap) → full-document load so the call still navigates. */
14
+ export declare function navigate(path: string, options?: NavigateOptions): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import { type NavState } from './store.ts';
2
+ /** Watch the navigation state from a React island.
3
+ *
4
+ * ```tsx
5
+ * import { useNav } from 'brustjs/client'
6
+ * function Crumb() {
7
+ * const { path, phase } = useNav()
8
+ * return <span>{phase === 'loading' ? 'Loading…' : path}</span>
9
+ * }
10
+ * ```
11
+ *
12
+ * The third arg (server snapshot) is the same getter — on the server the store
13
+ * holds its idle defaults (no `location`), so SSR renders the idle state and the
14
+ * client hydrates the real one on mount. */
15
+ export declare function useNav(): NavState;
@@ -0,0 +1,44 @@
1
+ import { type Signal } from '../store/signal.ts';
2
+ export type NavPhase = 'idle' | 'loading' | 'success' | 'error';
3
+ export interface NavState {
4
+ path: string;
5
+ search: string;
6
+ phase: NavPhase;
7
+ error: Error | null;
8
+ from: string | null;
9
+ to: string | null;
10
+ }
11
+ export interface NavStore {
12
+ path: Signal<string>;
13
+ search: Signal<string>;
14
+ phase: Signal<NavPhase>;
15
+ error: Signal<Error | null>;
16
+ from: Signal<string | null>;
17
+ to: Signal<string | null>;
18
+ }
19
+ type BeforeCb = (e: {
20
+ from: string;
21
+ to: string;
22
+ }) => void;
23
+ type NavCb = (e: NavState) => void;
24
+ type ErrorCb = (e: {
25
+ to: string;
26
+ error: Error;
27
+ }) => void;
28
+ export declare const nav: NavStore;
29
+ export declare function getNavState(): NavState;
30
+ export declare function subscribe(cb: NavCb): () => void;
31
+ export declare function onBeforeNavigate(cb: BeforeCb): () => void;
32
+ export declare function onNavigate(cb: NavCb): () => void;
33
+ export declare function onNavigateError(cb: ErrorCb): () => void;
34
+ export declare function __navInit(path: string, search: string): void;
35
+ export declare function __navStart(toPath: string, _toSearch: string): void;
36
+ export declare function __navCommit(toPath: string, toSearch: string): void;
37
+ export declare function __navError(toPath: string, error: unknown): void;
38
+ /** Register the SPA navigator implementation (bootstrap's swap). Last-write-wins;
39
+ * re-registration with the same closure (HMR / multiple chunks) is idempotent. */
40
+ export declare function registerNavigator(fn: (url: URL, replace: boolean) => Promise<void>): void;
41
+ /** @internal — the registered navigator, or null when no islands bootstrap loaded. */
42
+ export declare function _getNavigator(): ((url: URL, replace: boolean) => Promise<void>) | null;
43
+ export declare function __resetNavForTest(): void;
44
+ export {};
@@ -0,0 +1,20 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ /** Render a React element to a single HTML string, awaiting all Suspense
3
+ * boundaries via onAllReady. Used by navigationBranch — renderToString
4
+ * would only capture the shell + fallbacks, while renderBranchStreaming
5
+ * is for the streaming render path. */
6
+ export declare function renderToAwaitedString(element: ReactNode): Promise<string>;
7
+ export interface RenderFragmentOpts {
8
+ /** Request cookies visible to cookies()/session helpers inside the tree. */
9
+ cookies?: Record<string, string>;
10
+ }
11
+ /** Render a component to an HTML string with framework contexts scoped:
12
+ * request scope (cookies) ∘ request cache (cachedFetch/dedupe) ∘ store
13
+ * context (fresh per call — no cross-call leakage; concurrency-safe via
14
+ * AsyncLocalStorage). Suspense supported — the promise resolves when the
15
+ * whole tree is ready, never with fallbacks. Static HTML only: no island
16
+ * hydration markers/scripts (an `<Island>` inside the fragment SSRs its
17
+ * component but ships no hydration), no head/CSS collection, no streaming.
18
+ * Native/jinja fragments are served by `templates.render` instead.
19
+ * Errors reject with the component's error — no error-boundary semantics. */
20
+ export declare function renderFragment<P extends object>(Component: ComponentType<P>, props: P, opts?: RenderFragmentOpts): Promise<string>;
@@ -0,0 +1,9 @@
1
+ /** @internal — used by tests to reset the warn-once flag. */
2
+ export declare function _resetWarnedForTests(): void;
3
+ /** Splice `snippet` (a full `<script>…</script>`) into `body` immediately
4
+ * before the first `</head>` (case-insensitive on the four ASCII letters
5
+ * only). Returns the original body untouched if `snippet` is null/empty or if
6
+ * `</head>` is absent. */
7
+ export declare function injectActionPrefix(body: Uint8Array, snippet: string | null): Uint8Array;
8
+ export declare function configureActionPrefixSnippet(s: string | null): void;
9
+ export declare function getActionPrefixSnippet(): string | null;
@@ -0,0 +1,8 @@
1
+ /** @internal — used by the unit test suite to reset the warn-once flag. */
2
+ export declare function _resetWarnedForTests(): void;
3
+ /** Splice `<link rel="stylesheet" href="...">` tags into `body` immediately
4
+ * before the first occurrence of `</head>` (case-insensitive). Returns the
5
+ * original body untouched if `hrefs` is empty or if `</head>` is absent
6
+ * (warns once in the latter case). Renderer calls this on the first chunk
7
+ * only — see spec S"SSR <link> injection". */
8
+ export declare function injectCssLink(body: Uint8Array, hrefs: readonly string[]): Uint8Array;
@@ -0,0 +1,6 @@
1
+ /** @internal — used by tests to reset the warn-once flag. */
2
+ export declare function _resetWarnedForTests(): void;
3
+ /** Splice `snippet` into `body` immediately before the first `</head>`
4
+ * (case-insensitive on the four ASCII letters only). Returns the original body
5
+ * untouched if `snippet` is null/empty or if `</head>` is absent. */
6
+ export declare function injectDevClient(body: Uint8Array, snippet: string | null): Uint8Array;
@@ -0,0 +1,7 @@
1
+ /** Seed from generator.json at boot (main + worker). null → no injection. */
2
+ export declare function configureGeneratorMeta(meta: string | null): void;
3
+ export declare function getGeneratorMeta(): string | null;
4
+ /** Splice `metaTag` immediately before the first `</head>` (case-insensitive).
5
+ * No </head> in the chunk, empty tag, or an existing generator meta → body
6
+ * returned untouched. Byte-level (no decode) — safe with multibyte content. */
7
+ export declare function injectGeneratorMeta(body: Uint8Array, metaTag: string | null): Uint8Array;
@@ -0,0 +1,9 @@
1
+ /** @internal — used by tests to reset the warn-once flag. */
2
+ export declare function _resetWarnedForTests(): void;
3
+ /** Build the combined `<script type="application/json">` blob for every touched
4
+ * store. Returns '' when the snapshot is null/empty. */
5
+ export declare function buildStoreScripts(snap: Record<string, Record<string, unknown>> | null): string;
6
+ /** Splice the store snapshot `<script>`(s) into `body` immediately before the
7
+ * first `</head>`. Returns the original body untouched if the snapshot is
8
+ * null/empty or if `</head>` is absent. */
9
+ export declare function injectBrustStore(body: Uint8Array, snap: Record<string, Record<string, unknown>> | null): Uint8Array;
@@ -0,0 +1,45 @@
1
+ import { type ReactNode, type ComponentType } from 'react';
2
+ export interface RenderBranchStreamingArgs {
3
+ element: ReactNode;
4
+ /** The render's SAB sub-view (whole buffer at slots=1). All chunk encodes
5
+ * write at offset 0 of this view → automatically the slot's base. */
6
+ view: Uint8Array;
7
+ /** The render slot index, passed to every napi.renderChunk* call so Rust
8
+ * reads/writes the matching SAB sub-region. Default 0 (single-slot path). */
9
+ slot?: number;
10
+ workerId: bigint;
11
+ napi: {
12
+ renderChunk: (workerId: bigint, slot: number, len: number, sabBytes: Uint8Array) => Promise<void>;
13
+ renderChunkFinal: (workerId: bigint, slot: number, len: number, sabBytes: Uint8Array) => Promise<void>;
14
+ };
15
+ errorBoundary: ComponentType<{
16
+ error: Error;
17
+ }>;
18
+ /** Status for the successful (non-error) render. Default 200. Used by
19
+ * middleware-wrapped routes that want to set a non-200 status before
20
+ * the component runs (e.g. 201 from a server action redirect). */
21
+ status?: number;
22
+ /** Extra response headers injected by middleware (e.g. `x-render-ms`).
23
+ * Merged into the meta envelope's `headers` map. */
24
+ headers?: Record<string, string>;
25
+ /** The matched route's fullPath (e.g. '/' or '/blog/{slug}'). Used to
26
+ * combine global CSS hrefs with per-route CSS hrefs before injection. */
27
+ routePath?: string;
28
+ /** Per-request store snapshot collected after loaders run. Injected as a
29
+ * `<script data-brust-store="…">` blob before `</head>` (buffering) or into
30
+ * the streaming first-chunk prepend. Null/undefined → no injection. */
31
+ storeSnapshot?: Record<string, Record<string, unknown>> | null;
32
+ /** Inject the islands importmap + bootstrap even when no <Island> rendered.
33
+ * SSG fallback shells need the bootstrap (client takeover runtime) on pages
34
+ * that may have zero real islands. */
35
+ forceIslands?: boolean;
36
+ }
37
+ /** JSON.stringify the per-chunk meta. Defaults match the renderToString
38
+ * path so single-chunk responses keep their existing wire shape. */
39
+ export declare function makeMeta(opts: {
40
+ status: number;
41
+ streaming: boolean;
42
+ contentType?: string;
43
+ headers?: Record<string, string>;
44
+ }): string;
45
+ export declare function renderBranchStreaming(args: RenderBranchStreamingArgs): Promise<void>;
@@ -0,0 +1,16 @@
1
+ interface RequestScope {
2
+ ctx: Map<string, unknown>;
3
+ reqCookies: Record<string, string>;
4
+ setCookies: string[];
5
+ }
6
+ /** Open a per-request scope. `reqCookies` is the parsed incoming Cookie header
7
+ * (BrustRequest.cookies). Staged Set-Cookie strings accumulate in `setCookies`
8
+ * and are flushed by routes.ts onto the response headers. */
9
+ export declare function runInRequestScope<T>(reqCookies: Record<string, string>, fn: () => T): T;
10
+ /** Per-request scratch Map for apps building request-scoped state on top of the
11
+ * module-global store. Throws when called outside a request scope. */
12
+ export declare function getRequestContext(): Map<string, unknown>;
13
+ /** Internal — the active scope (or undefined). Used by cookies.ts and the
14
+ * routes.ts flush. Not re-exported from the package entry. */
15
+ export declare function __scope(): RequestScope | undefined;
16
+ export {};