@streetui/renderer 1.0.0

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.
@@ -0,0 +1,373 @@
1
+ import { DOMAdapter, ServerDOMAdapter } from '@streetui/dom';
2
+ import { GraphNode, ApplicationGraph } from '@streetui/graph';
3
+ import { CleanupRegistry, SemanticNodeType } from '@streetui/core';
4
+ import { ReadonlySignal } from '@streetui/state';
5
+ import { CompiledApplication } from '@streetui/compiler';
6
+ import { StreetRenderer, RenderHandle } from '@streetui/runtime';
7
+
8
+ /**
9
+ * NodeInstance — the renderer's live counterpart to a GraphNode.
10
+ *
11
+ * Tracks the actual DOM node(s), all signal subscriptions that drive
12
+ * targeted DOM updates, and DOM event listener teardowns.
13
+ */
14
+
15
+ declare class NodeInstance {
16
+ readonly graphNode: GraphNode;
17
+ /** The primary DOM node for this instance (element or text node). */
18
+ domNode: Node;
19
+ readonly children: NodeInstance[];
20
+ readonly cleanup: CleanupRegistry;
21
+ constructor(graphNode: GraphNode, domNode: Node);
22
+ addChild(child: NodeInstance): void;
23
+ /** Subscribe to a signal; auto-cleanup on unmount. */
24
+ trackSignal<T>(sig: ReadonlySignal<T>, handler: (v: T) => void): void;
25
+ /** Register a raw cleanup fn (DOM event removal, etc.). */
26
+ trackCleanup(fn: () => void): void;
27
+ dispose(): void;
28
+ }
29
+
30
+ /**
31
+ * Hydration diagnostics — dev-only, opt-in explanations of hydration mismatches.
32
+ *
33
+ * Hydration is self-repairing: when the server-rendered DOM does not match the
34
+ * graph at a position, the renderer mounts a fresh subtree in place and drops
35
+ * the offending element (see `hydrateChildren` in `hydrate.ts`). That recovery
36
+ * is silent by design — a local mismatch must never tear down the whole app.
37
+ *
38
+ * During development, though, a silent repair hides a real problem (usually a
39
+ * server/client divergence). A `HydrationDiagnosticSink` can be attached to the
40
+ * renderer to *observe* those repairs without changing them: for every mismatch
41
+ * the renderer reports what it expected, what it found, where, and what it did
42
+ * to recover. Nothing is thrown, nothing is mutated differently, and when no
43
+ * sink is attached there is zero additional work on the hydration path.
44
+ */
45
+ /** What kind of divergence the hydrator encountered at a position. */
46
+ type HydrationMismatchType = 'tag-mismatch' | 'missing-element' | 'surplus-element';
47
+ /** A single, fully-described hydration divergence and the repair taken. */
48
+ interface HydrationDiagnostic {
49
+ /** The category of mismatch. */
50
+ readonly type: HydrationMismatchType;
51
+ /** The tag the graph expected at this position (null for a surplus element). */
52
+ readonly expected: string | null;
53
+ /** The tag actually found in the server DOM (null for a missing element). */
54
+ readonly found: string | null;
55
+ /** A human-readable path to the position, e.g. `app / page[0] / section[1]`. */
56
+ readonly path: string;
57
+ /** The graph node id involved, when one exists (null for surplus DOM). */
58
+ readonly nodeId: string | null;
59
+ /** The semantic node type involved, when one exists (null for surplus DOM). */
60
+ readonly nodeType: string | null;
61
+ /** The recovery action the renderer performed. */
62
+ readonly action: string;
63
+ /** A single-line, developer-facing summary of the whole diagnostic. */
64
+ readonly message: string;
65
+ }
66
+ /**
67
+ * Receives hydration diagnostics as they are discovered. Kept intentionally
68
+ * tiny so any logger — `console`, a test collector, a `DiagnosticSink` — can
69
+ * satisfy it. Implementations must not throw.
70
+ */
71
+ interface HydrationDiagnosticSink {
72
+ report(diagnostic: HydrationDiagnostic): void;
73
+ }
74
+ /** Build the canonical one-line message for a diagnostic. */
75
+ declare function formatHydrationDiagnostic(d: Omit<HydrationDiagnostic, 'message'>): string;
76
+ /**
77
+ * A ready-made sink that accumulates diagnostics into an array — the shape most
78
+ * useful for tests and for a DevTools panel. The returned `diagnostics` array is
79
+ * appended to in-place as repairs happen.
80
+ */
81
+ declare function createHydrationDiagnosticCollector(): {
82
+ readonly sink: HydrationDiagnosticSink;
83
+ readonly diagnostics: HydrationDiagnostic[];
84
+ };
85
+ /**
86
+ * A sink that forwards each diagnostic to a `console`-like logger as a single
87
+ * warning line. Handy default when you just want the messages surfaced in dev.
88
+ */
89
+ declare function consoleHydrationDiagnosticSink(logger?: {
90
+ warn(message: string): void;
91
+ }): HydrationDiagnosticSink;
92
+
93
+ /**
94
+ * RenderContext — shared state for a single mount operation.
95
+ *
96
+ * Passed through the render pipeline so every sub-function has access
97
+ * to the DOM adapter, graph, and instance map without prop-drilling.
98
+ */
99
+
100
+ interface RenderContext {
101
+ readonly dom: DOMAdapter;
102
+ readonly graph: ApplicationGraph;
103
+ /** Maps GraphNode.id → its live NodeInstance */
104
+ readonly instances: Map<string, NodeInstance>;
105
+ /** The root container element. */
106
+ readonly container: Element;
107
+ /**
108
+ * Optional dev-only sink that observes hydration mismatch repairs. When
109
+ * absent (the default) the hydration path does no extra work — this is how
110
+ * DevTools/diagnostics stay off the production runtime path.
111
+ */
112
+ readonly hydrationDiagnostics?: HydrationDiagnosticSink;
113
+ }
114
+ declare function createRenderContext(dom: DOMAdapter, graph: ApplicationGraph, container: Element, hydrationDiagnostics?: HydrationDiagnosticSink): RenderContext;
115
+
116
+ /**
117
+ * Attribute and property application helpers.
118
+ *
119
+ * Decides whether a prop should be set as a DOM attribute or a JS property,
120
+ * handling special cases (boolean attrs, event-like props, style, class).
121
+ */
122
+
123
+ declare function applyProp(dom: DOMAdapter, element: Element, name: string, value: unknown): void;
124
+ declare function patchProp(dom: DOMAdapter, element: Element, name: string, oldValue: unknown, newValue: unknown): void;
125
+
126
+ /**
127
+ * Event wiring for the renderer.
128
+ *
129
+ * Given a GraphNode with event descriptors, this wires DOM listeners
130
+ * that call the handlers stored in the graph's handler registry.
131
+ */
132
+
133
+ declare function wireEvents(dom: DOMAdapter, graph: ApplicationGraph, node: GraphNode, element: Element, instance: NodeInstance): void;
134
+
135
+ /**
136
+ * Initial mount — creates DOM nodes for every GraphNode and
137
+ * attaches them into the container.
138
+ *
139
+ * This is a recursive depth-first walk. For each GraphNode:
140
+ * 1. Create the DOM element (or text node)
141
+ * 2. Apply props/attributes
142
+ * 3. Wire events
143
+ * 4. Wire signal subscriptions for reactive props
144
+ * 5. Recurse into children
145
+ * 6. Insert into the DOM
146
+ */
147
+
148
+ declare function mountGraph(ctx: RenderContext): NodeInstance;
149
+ declare function mountNode(ctx: RenderContext, graphNode: GraphNode, parentDom: Node): NodeInstance;
150
+ /**
151
+ * Per-node-type reactive-binding factories. Each returns the `onUpdate`
152
+ * callback that `wireSignalBindings` invokes when a bound signal changes.
153
+ * Extracted so both the browser mount path and the hydration path apply the
154
+ * exact same DOM mutation semantics for each prop — no duplicated rendering
155
+ * logic.
156
+ */
157
+ declare function textUpdate(dom: DOMAdapter, el: Element, textNode: Text): (propKey: string, value: unknown) => void;
158
+ declare function headingUpdate(dom: DOMAdapter, el: Element): (propKey: string, value: unknown) => void;
159
+ declare function inputUpdate(dom: DOMAdapter, el: Element): (propKey: string, value: unknown) => void;
160
+ declare function buttonUpdate(dom: DOMAdapter, el: Element): (propKey: string, value: unknown) => void;
161
+ declare function applyNodeProps(ctx: RenderContext, graphNode: GraphNode, el: Element): void;
162
+ declare function wireSignalBindings(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, onUpdate: (propKey: string, value: unknown) => void): void;
163
+ /**
164
+ * Subscribe a reactive-list instance to its driving signal. On each change the
165
+ * DSL-registered build factory produces the desired child graph nodes, which
166
+ * are reconciled against the live DOM with the keyed reconciler.
167
+ */
168
+ declare function wireReactiveList(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, el: Element): void;
169
+
170
+ /**
171
+ * Patch — targeted DOM updates driven by signal changes.
172
+ *
173
+ * When a signal fires, we look up the NodeInstance and apply
174
+ * only the changed prop — no full re-render, no tree diffing.
175
+ */
176
+
177
+ declare function patchNode(ctx: RenderContext, graphNode: GraphNode, propKey: string, newValue: unknown): void;
178
+
179
+ /**
180
+ * Reconciliation — diff-based child list updates.
181
+ *
182
+ * When the children of a node change (e.g. a list driven by state),
183
+ * this reconciler:
184
+ * 1. Matches old instances to new graph nodes by key
185
+ * 2. Reuses matched instances (updates their props)
186
+ * 3. Applies a targeted content update to a reused item whose data changed
187
+ * 4. Creates new instances for additions
188
+ * 5. Removes stale instances (and prunes their handler registrations)
189
+ * 6. Moves DOM nodes to match new order
190
+ *
191
+ * This is keyed reconciliation over the semantic graph — there is no virtual
192
+ * DOM. A reused item keeps its own DOM element; only its changed content is
193
+ * updated in place (falling back to remounting a subtree only where its shape
194
+ * actually changed).
195
+ */
196
+
197
+ type MountFn = (node: GraphNode, parent: Element) => NodeInstance;
198
+ interface ReconcileResult {
199
+ /** Instances in the new order. */
200
+ instances: NodeInstance[];
201
+ /** Instances that were removed and must be disposed. */
202
+ removed: NodeInstance[];
203
+ }
204
+ /**
205
+ * Reconcile children of a container element against a new list of graph nodes.
206
+ *
207
+ * @param ctx Render context
208
+ * @param parentDom The DOM parent element
209
+ * @param oldInstances Current child instances (in order)
210
+ * @param newNodes New graph children (in desired order)
211
+ * @param mountFn Factory to create a new NodeInstance for a graph node
212
+ */
213
+ declare function reconcileChildren(ctx: RenderContext, parentDom: Element, oldInstances: NodeInstance[], newNodes: readonly GraphNode[], mountFn: MountFn): ReconcileResult;
214
+
215
+ /**
216
+ * StreetUI Renderer — framework-owned DOM renderer.
217
+ *
218
+ * No React. No Vue. No virtual-dom. No external rendering library.
219
+ *
220
+ * Pipeline:
221
+ * CompiledApplication
222
+ * → mountGraph (creates all DOM nodes)
223
+ * → signal subscriptions drive patchNode (targeted updates)
224
+ * → flush() propagates any pending scheduler jobs
225
+ * → unmount() disposes everything
226
+ */
227
+
228
+ interface StreetRendererOptions {
229
+ /** Override the DOM adapter (e.g. for testing). Defaults to BrowserDOMAdapter. */
230
+ readonly domAdapter?: DOMAdapter;
231
+ /**
232
+ * Optional dev-only sink that observes hydration mismatch repairs. Attach one
233
+ * to surface server/client divergences during development; leave it unset in
234
+ * production so hydration does no extra work.
235
+ */
236
+ readonly hydrationDiagnostics?: HydrationDiagnosticSink;
237
+ }
238
+ declare class StreetRendererImpl implements StreetRenderer {
239
+ private readonly _dom;
240
+ private readonly _hydrationDiagnostics?;
241
+ constructor(options?: StreetRendererOptions);
242
+ mount(compiled: CompiledApplication, container: Element): RenderHandle;
243
+ /**
244
+ * Hydrate a container that already holds server-rendered HTML for this
245
+ * application. Instead of recreating the DOM, it walks the semantic graph
246
+ * against the existing nodes, adopting matching elements and attaching
247
+ * behavior (events + signal subscriptions). Mismatched subtrees are locally
248
+ * replaced. Returns the same handle type as `mount`.
249
+ */
250
+ hydrate(compiled: CompiledApplication, container: Element): RenderHandle;
251
+ private _wireSignals;
252
+ }
253
+ /**
254
+ * Create the default StreetUI renderer using the browser's DOM APIs.
255
+ */
256
+ declare function createRenderer(options?: StreetRendererOptions): StreetRendererImpl;
257
+
258
+ /**
259
+ * StreetRenderHandle — the live handle returned by both `mount` and `hydrate`.
260
+ *
261
+ * Owns teardown for a mounted/hydrated application: disposes every NodeInstance
262
+ * (removing event listeners and signal subscriptions) and clears the container
263
+ * through the DOM adapter (never raw browser globals), so the same handle works
264
+ * for browser and — in principle — server-driven teardown.
265
+ */
266
+
267
+ declare class StreetRenderHandle implements RenderHandle {
268
+ private _disposed;
269
+ private readonly _ctx;
270
+ private readonly _rootInstance;
271
+ constructor(ctx: RenderContext, rootInstance: NodeInstance);
272
+ flush(): void;
273
+ unmount(): void;
274
+ }
275
+
276
+ /**
277
+ * Hydration — attach a live StreetUI runtime to server-rendered HTML.
278
+ *
279
+ * `hydrate` walks the semantic application graph top-down against the DOM that
280
+ * the server already produced. For every graph node it *adopts* the matching
281
+ * existing element (creating a `NodeInstance` that points at it) and attaches
282
+ * behavior — event listeners and signal subscriptions — using the exact same
283
+ * helpers the browser mount path uses (`wireEvents`, `wireSignalBindings`,
284
+ * `wireReactiveList`, and the per-type update factories). Nothing is recreated
285
+ * when the DOM matches.
286
+ *
287
+ * Matching is positional and works because every non-application graph node
288
+ * maps to exactly one element (see mount.ts). When the element at a position
289
+ * does not match the expected tag (or is missing), only that subtree is
290
+ * repaired: the fresh subtree is mounted and spliced into place, leaving the
291
+ * rest of the hydrated tree untouched. A local mismatch never tears down the
292
+ * whole app.
293
+ */
294
+
295
+ /** Hydrate the whole application graph against `ctx.container`. */
296
+ declare function hydrateGraph(ctx: RenderContext): NodeInstance;
297
+
298
+ /**
299
+ * SSR state transfer (dehydration) — move server-resolved data to the client.
300
+ *
301
+ * When the server resolves resources before rendering, their data must reach
302
+ * the client so hydration can seed them (via `resource({ initialData })`)
303
+ * instead of refetching. StreetUI does this with a single, framework-scoped
304
+ * `<script>` payload rather than blindly interpolating `JSON.stringify` into
305
+ * markup.
306
+ *
307
+ * Safety (v0.4 rule #16): the JSON is emitted into a
308
+ * `<script type="application/json">` block — an inert data island the browser
309
+ * never executes — and every character that could terminate that block or be
310
+ * reinterpreted by the HTML/JS parser is escaped to its `\uXXXX` form. Because
311
+ * `<` inside JSON parses back to `<`, the payload round-trips exactly
312
+ * while being impossible to break out of. This is deterministic (stable key
313
+ * order is the caller's responsibility) and typed at the boundary as
314
+ * `Record<string, unknown>` — never `any`.
315
+ */
316
+
317
+ /** Attribute marking StreetUI's state island so the client can find it. */
318
+ declare const STATE_MARKER_ATTR = "data-streetui-state";
319
+ /**
320
+ * Serialize a state map to an HTML `<script>` island for inclusion in the
321
+ * server-rendered document (typically just before the closing tag of the
322
+ * mount container). Returns an empty string for an empty map.
323
+ */
324
+ declare function serializeState(state: Record<string, unknown>): string;
325
+ /**
326
+ * Read the state island back on the client. Searches `root` for StreetUI's
327
+ * state `<script>` and parses it. Returns an empty object when absent or
328
+ * unparseable (hydration then proceeds as a cold client render). Routed through
329
+ * the DOM adapter so it is testable and never assumes a global `document`.
330
+ */
331
+ declare function readState(dom: DOMAdapter, root: Element | Document): Record<string, unknown>;
332
+
333
+ /**
334
+ * Server-side rendering — `renderToString`.
335
+ *
336
+ * Runs the *exact same* mount pipeline used in the browser (`mountGraph`), but
337
+ * against a `ServerDOMAdapter` that builds a lightweight in-memory node tree
338
+ * instead of a real browser DOM. The tree is then serialized to a normal HTML
339
+ * string. Because both browser and server share the DSL → Compiler → Graph →
340
+ * Runtime → Renderer pipeline, there is no second, SSR-specific renderer and no
341
+ * virtual DOM.
342
+ *
343
+ * Lifecycle (v0.4 rule #20): the initial synchronous mount may open signal
344
+ * subscriptions (via `wireSignalBindings`/`wireReactiveList`). On the server
345
+ * those would be live forever, so once the HTML is serialized we dispose the
346
+ * root instance — tearing down every subscription and listener. SSR therefore
347
+ * has a *render* lifecycle only; the live *runtime* lifecycle is established
348
+ * later on the client by `hydrate`.
349
+ */
350
+
351
+ interface RenderToStringOptions {
352
+ /**
353
+ * Override the server DOM adapter (rarely needed). Defaults to a fresh
354
+ * `ServerDOMAdapter` per call so concurrent renders never share state.
355
+ */
356
+ readonly domAdapter?: ServerDOMAdapter;
357
+ }
358
+ /**
359
+ * Render a compiled StreetUI application to an HTML string.
360
+ *
361
+ * The returned markup contains only the application's own elements (the
362
+ * synthetic container is not emitted), so callers embed it wherever they mount
363
+ * on the client — e.g. inside `<div id="app">…</div>`.
364
+ */
365
+ declare function renderToString(compiled: CompiledApplication, options?: RenderToStringOptions): string;
366
+
367
+ /**
368
+ * Maps semantic node types to HTML tag names.
369
+ */
370
+
371
+ declare function resolveTag(type: SemanticNodeType): string;
372
+
373
+ export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type ReconcileResult, type RenderContext, type RenderToStringOptions, STATE_MARKER_ATTR, StreetRenderHandle, StreetRendererImpl, type StreetRendererOptions, applyNodeProps, applyProp, buttonUpdate, consoleHydrationDiagnosticSink, createHydrationDiagnosticCollector, createRenderContext, createRenderer, formatHydrationDiagnostic, headingUpdate, hydrateGraph, inputUpdate, mountGraph, mountNode, patchNode, patchProp, readState, reconcileChildren, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireReactiveList, wireSignalBindings };