brustjs 0.1.50-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 (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,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;
@@ -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>;