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.
- package/package.json +39 -15
- package/runtime/cache-sync.ts +291 -0
- package/runtime/cache.ts +4 -0
- package/runtime/cli/dev.ts +7 -0
- package/runtime/cli/native-routes-emit.ts +147 -1
- package/runtime/config.ts +42 -0
- package/runtime/index.d.ts +63 -0
- package/runtime/index.js +57 -52
- package/runtime/index.ts +108 -9
- package/runtime/native/runtime.ts +220 -7
- package/runtime/render/fragment.ts +87 -0
- package/runtime/routes.ts +225 -48
- package/runtime/templates.ts +47 -0
- package/runtime/treaty.ts +24 -1
- package/types/action-error.d.ts +18 -0
- package/types/cache-sync.d.ts +42 -0
- package/types/cache.d.ts +20 -0
- package/types/cli/help.d.ts +28 -0
- package/types/cli/jinja-staleness.d.ts +14 -0
- package/types/cli/native-routes-emit.d.ts +217 -0
- package/types/cli/new.d.ts +30 -0
- package/types/cli/templates.d.ts +39 -0
- package/types/client/index.d.ts +14 -0
- package/types/config.d.ts +42 -0
- package/types/cookies.d.ts +25 -0
- package/types/create.d.ts +1 -0
- package/types/css/build.d.ts +11 -0
- package/types/css/component-build.d.ts +17 -0
- package/types/css/component-loader.d.ts +8 -0
- package/types/css/manifest.d.ts +21 -0
- package/types/css/process-modules.d.ts +31 -0
- package/types/css/route-deps.d.ts +20 -0
- package/types/css/scan-imports.d.ts +13 -0
- package/types/css.d.ts +16 -0
- package/types/define-actions.d.ts +133 -0
- package/types/dev/client.d.ts +8 -0
- package/types/dev/coordinator.d.ts +33 -0
- package/types/dev/inject.d.ts +6 -0
- package/types/dev/jinja-reload.d.ts +7 -0
- package/types/dev/tui.d.ts +35 -0
- package/types/dev/watcher.d.ts +34 -0
- package/types/dev/worker-registry.d.ts +17 -0
- package/types/dev/ws-channel.d.ts +39 -0
- package/types/generator.d.ts +23 -0
- package/types/index.d.ts +222 -0
- package/types/islands/brust-page.d.ts +74 -0
- package/types/islands/build.d.ts +49 -0
- package/types/islands/chunk-id.d.ts +10 -0
- package/types/islands/importmap.d.ts +2 -0
- package/types/islands/island.d.ts +65 -0
- package/types/islands/isr-jsx.d.ts +31 -0
- package/types/islands/native-render.d.ts +89 -0
- package/types/loader-cache.d.ts +18 -0
- package/types/mcp/extractor.d.ts +14 -0
- package/types/mcp/manifest.d.ts +23 -0
- package/types/mcp/schema.d.ts +19 -0
- package/types/mcp/server.d.ts +15 -0
- package/types/md/emit.d.ts +72 -0
- package/types/md/render.d.ts +80 -0
- package/types/md/routes.d.ts +119 -0
- package/types/md/scan.d.ts +34 -0
- package/types/md/slug.d.ts +1 -0
- package/types/native/build.d.ts +30 -0
- package/types/native/index.d.ts +2 -0
- package/types/native/runtime.d.ts +52 -0
- package/types/navigation/active-nav.d.ts +2 -0
- package/types/navigation/index.d.ts +5 -0
- package/types/navigation/navigate.d.ts +14 -0
- package/types/navigation/react.d.ts +15 -0
- package/types/navigation/store.d.ts +44 -0
- package/types/render/fragment.d.ts +20 -0
- package/types/render/inject-action-prefix.d.ts +9 -0
- package/types/render/inject-css-link.d.ts +8 -0
- package/types/render/inject-dev-client.d.ts +6 -0
- package/types/render/inject-generator.d.ts +7 -0
- package/types/render/inject-store.d.ts +9 -0
- package/types/render/stream.d.ts +45 -0
- package/types/request-context.d.ts +16 -0
- package/types/routes.d.ts +506 -0
- package/types/sse/handler.d.ts +22 -0
- package/types/standard-schema.d.ts +31 -0
- package/types/store/define-store.d.ts +31 -0
- package/types/store/index.d.ts +5 -0
- package/types/store/react.d.ts +2 -0
- package/types/store/serialize.d.ts +5 -0
- package/types/store/server-context.d.ts +4 -0
- package/types/store/signal.d.ts +18 -0
- package/types/templates.d.ts +18 -0
- package/types/treaty.d.ts +70 -0
- 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,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,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>;
|