@streetui/renderer 1.6.0 → 1.7.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.
- package/dist/index.cjs +162 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +165 -5
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.d.ts
CHANGED
|
@@ -110,8 +110,18 @@ interface RenderContext {
|
|
|
110
110
|
* DevTools/diagnostics stay off the production runtime path.
|
|
111
111
|
*/
|
|
112
112
|
readonly hydrationDiagnostics?: HydrationDiagnosticSink;
|
|
113
|
+
/**
|
|
114
|
+
* Optional SSR-only static-subtree plan (v1.7). Maps a maximal
|
|
115
|
+
* static-subtree root GraphNode.id → its precomputed, verbatim HTML string.
|
|
116
|
+
* Present only on the server render path when a plan has been built; on the
|
|
117
|
+
* browser mount path it is always `undefined`, so the client hot path is
|
|
118
|
+
* unaffected (a single `=== undefined` check short-circuits). When a mounted
|
|
119
|
+
* node's id is in this map, the renderer emits the precomputed HTML via
|
|
120
|
+
* `dom.createRawHTML` instead of recursively constructing the subtree.
|
|
121
|
+
*/
|
|
122
|
+
readonly staticHTML?: ReadonlyMap<string, string>;
|
|
113
123
|
}
|
|
114
|
-
declare function createRenderContext(dom: DOMAdapter, graph: ApplicationGraph, container: Element, hydrationDiagnostics?: HydrationDiagnosticSink): RenderContext;
|
|
124
|
+
declare function createRenderContext(dom: DOMAdapter, graph: ApplicationGraph, container: Element, hydrationDiagnostics?: HydrationDiagnosticSink, staticHTML?: ReadonlyMap<string, string>): RenderContext;
|
|
115
125
|
|
|
116
126
|
/**
|
|
117
127
|
* Attribute and property application helpers.
|
|
@@ -168,6 +178,20 @@ declare function wireSignalBindings(ctx: RenderContext, graphNode: GraphNode, in
|
|
|
168
178
|
* eager build factory (it only ever renders 0..1 branch, so eager is fine).
|
|
169
179
|
*/
|
|
170
180
|
declare function wireReactiveList(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, el: Element): void;
|
|
181
|
+
/**
|
|
182
|
+
* Attach overlay focus/keyboard behavior to a mounted portal. Server-safe: on
|
|
183
|
+
* the server `dom.body()` is null so this returns immediately (SSR emits inert
|
|
184
|
+
* markup, no focus concept). A plain portal has no `__overlay__` descriptor, so
|
|
185
|
+
* this also returns immediately — the behavior is purely additive.
|
|
186
|
+
*
|
|
187
|
+
* The panel is mounted/unmounted by the portal's inner `when(open, …)`, whose
|
|
188
|
+
* signal subscription is registered *before* this one (the conditional child is
|
|
189
|
+
* mounted earlier in the portal branch). Signal subscribers fire synchronously
|
|
190
|
+
* in subscription order, so on open→true the panel DOM exists before we move
|
|
191
|
+
* focus into it, and on open→false the panel is torn down before we restore
|
|
192
|
+
* focus. All listeners are tracked on the instance and torn down on unmount.
|
|
193
|
+
*/
|
|
194
|
+
declare function wireOverlayBehavior(ctx: RenderContext, graphNode: GraphNode, instance: NodeInstance, target: Element): void;
|
|
171
195
|
|
|
172
196
|
/**
|
|
173
197
|
* Patch — targeted DOM updates driven by signal changes.
|
|
@@ -393,6 +417,18 @@ interface RenderToStringOptions {
|
|
|
393
417
|
* `ServerDOMAdapter` per call so concurrent renders never share state.
|
|
394
418
|
*/
|
|
395
419
|
readonly domAdapter?: ServerDOMAdapter;
|
|
420
|
+
/**
|
|
421
|
+
* @internal — testing/benchmark knob for the v1.7 static SSR plan.
|
|
422
|
+
*
|
|
423
|
+
* `undefined` (default): use the per-app cached plan (build once, reuse).
|
|
424
|
+
* `null`: disable the plan entirely — the exact v1.6 runtime mount path, used
|
|
425
|
+
* by the byte-identity gate and A/B benchmark as the "legacy" baseline.
|
|
426
|
+
* a map: use this explicit plan.
|
|
427
|
+
*
|
|
428
|
+
* Not part of the supported public API; output is byte-identical regardless
|
|
429
|
+
* of this value (§8).
|
|
430
|
+
*/
|
|
431
|
+
readonly staticPlan?: ReadonlyMap<string, string> | null;
|
|
396
432
|
}
|
|
397
433
|
/**
|
|
398
434
|
* Render a compiled StreetUI application to an HTML string.
|
|
@@ -409,4 +445,4 @@ declare function renderToString(compiled: CompiledApplication, options?: RenderT
|
|
|
409
445
|
|
|
410
446
|
declare function resolveTag(type: SemanticNodeType): string;
|
|
411
447
|
|
|
412
|
-
export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type PlanEntry, 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, reconcileChildrenByPlan, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireReactiveList, wireSignalBindings };
|
|
448
|
+
export { type HydrationDiagnostic, type HydrationDiagnosticSink, type HydrationMismatchType, type MountFn, NodeInstance, type PlanEntry, 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, reconcileChildrenByPlan, renderToString, resolveTag, serializeState, textUpdate, wireEvents, wireOverlayBehavior, wireReactiveList, wireSignalBindings };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// src/render-context.ts
|
|
2
|
-
function createRenderContext(dom, graph, container, hydrationDiagnostics) {
|
|
2
|
+
function createRenderContext(dom, graph, container, hydrationDiagnostics, staticHTML) {
|
|
3
3
|
return {
|
|
4
4
|
dom,
|
|
5
5
|
graph,
|
|
6
6
|
instances: /* @__PURE__ */ new Map(),
|
|
7
7
|
container,
|
|
8
|
-
...hydrationDiagnostics !== void 0 ? { hydrationDiagnostics } : {}
|
|
8
|
+
...hydrationDiagnostics !== void 0 ? { hydrationDiagnostics } : {},
|
|
9
|
+
...staticHTML !== void 0 ? { staticHTML } : {}
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -137,6 +138,16 @@ function wireEvents(dom, graph, node, element, instance) {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// src/mount.ts
|
|
142
|
+
import {
|
|
143
|
+
focusInitial,
|
|
144
|
+
trapFocus,
|
|
145
|
+
containFocus,
|
|
146
|
+
onEscape,
|
|
147
|
+
saveFocus,
|
|
148
|
+
restoreFocus
|
|
149
|
+
} from "@streetui/dom";
|
|
150
|
+
|
|
140
151
|
// src/tag-map.ts
|
|
141
152
|
var TAG_MAP = {
|
|
142
153
|
application: "div",
|
|
@@ -155,7 +166,12 @@ var TAG_MAP = {
|
|
|
155
166
|
component: "div",
|
|
156
167
|
slot: "div",
|
|
157
168
|
fragment: "div",
|
|
158
|
-
"reactive-list": "ul"
|
|
169
|
+
"reactive-list": "ul",
|
|
170
|
+
// A portal renders as a neutral inline anchor <div> at its declaration site;
|
|
171
|
+
// its children are relocated to a document.body container on the browser
|
|
172
|
+
// (see the portal branch in mount.ts). On the server (no body) it renders
|
|
173
|
+
// inline, so the anchor tag is what SSR/hydration positionally match on.
|
|
174
|
+
portal: "div"
|
|
159
175
|
};
|
|
160
176
|
function resolveTag(type) {
|
|
161
177
|
return TAG_MAP[type] ?? "div";
|
|
@@ -434,6 +450,17 @@ function mountGraph(ctx) {
|
|
|
434
450
|
}
|
|
435
451
|
function mountNode(ctx, graphNode, parentDom) {
|
|
436
452
|
const { dom, graph } = ctx;
|
|
453
|
+
const staticHTML = ctx.staticHTML;
|
|
454
|
+
if (staticHTML !== void 0 && dom.createRawHTML !== void 0) {
|
|
455
|
+
const precomputed = staticHTML.get(graphNode.id);
|
|
456
|
+
if (precomputed !== void 0) {
|
|
457
|
+
const raw = dom.createRawHTML(precomputed);
|
|
458
|
+
dom.appendChild(parentDom, raw);
|
|
459
|
+
const instance2 = new NodeInstance(graphNode, raw);
|
|
460
|
+
ctx.instances.set(graphNode.id, instance2);
|
|
461
|
+
return instance2;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
437
464
|
if (graphNode.type === "application") {
|
|
438
465
|
const instance2 = new NodeInstance(graphNode, parentDom);
|
|
439
466
|
ctx.instances.set(graphNode.id, instance2);
|
|
@@ -556,6 +583,28 @@ function mountNode(ctx, graphNode, parentDom) {
|
|
|
556
583
|
wireReactiveList(ctx, graphNode, instance2, el2);
|
|
557
584
|
return instance2;
|
|
558
585
|
}
|
|
586
|
+
if (graphNode.type === "portal") {
|
|
587
|
+
const anchor = dom.createElement(resolveTag("portal"));
|
|
588
|
+
dom.setAttribute(anchor, "data-streetui-portal", "");
|
|
589
|
+
applyNodeProps(ctx, graphNode, anchor);
|
|
590
|
+
const instance2 = new NodeInstance(graphNode, anchor);
|
|
591
|
+
ctx.instances.set(graphNode.id, instance2);
|
|
592
|
+
const body = dom.body();
|
|
593
|
+
let target = anchor;
|
|
594
|
+
if (body !== null) {
|
|
595
|
+
const portalContainer = dom.createElement("div");
|
|
596
|
+
dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
|
|
597
|
+
dom.appendChild(body, portalContainer);
|
|
598
|
+
instance2.trackCleanup(() => dom.removeChild(body, portalContainer));
|
|
599
|
+
target = portalContainer;
|
|
600
|
+
}
|
|
601
|
+
for (const child of graphNode.children) {
|
|
602
|
+
instance2.addChild(mountNode(ctx, child, target));
|
|
603
|
+
}
|
|
604
|
+
dom.appendChild(parentDom, anchor);
|
|
605
|
+
wireOverlayBehavior(ctx, graphNode, instance2, target);
|
|
606
|
+
return instance2;
|
|
607
|
+
}
|
|
559
608
|
const tag = resolveTag(graphNode.type);
|
|
560
609
|
const el = dom.createElement(tag);
|
|
561
610
|
applyNodeProps(ctx, graphNode, el);
|
|
@@ -705,6 +754,44 @@ function forgetInstance(ctx, instance) {
|
|
|
705
754
|
ctx.instances.delete(instance.graphNode.id);
|
|
706
755
|
for (const child of instance.children) forgetInstance(ctx, child);
|
|
707
756
|
}
|
|
757
|
+
function wireOverlayBehavior(ctx, graphNode, instance, target) {
|
|
758
|
+
const { dom, graph } = ctx;
|
|
759
|
+
if (dom.body() === null) return;
|
|
760
|
+
const descFn = graph.getHandler(`__overlay__${graphNode.id}`);
|
|
761
|
+
if (descFn === void 0) return;
|
|
762
|
+
const desc = descFn();
|
|
763
|
+
const openSig = desc.open;
|
|
764
|
+
if (openSig === void 0 || typeof openSig.subscribe !== "function") return;
|
|
765
|
+
let active = [];
|
|
766
|
+
let saved = null;
|
|
767
|
+
const teardown = () => {
|
|
768
|
+
for (const fn of active) fn();
|
|
769
|
+
active = [];
|
|
770
|
+
};
|
|
771
|
+
const onOpenChange = (isOpen) => {
|
|
772
|
+
if (isOpen) {
|
|
773
|
+
if (desc.restoreFocus) saved = saveFocus(dom);
|
|
774
|
+
if (desc.takesFocus) focusInitial(dom, target, desc.initialFocusId);
|
|
775
|
+
if (desc.modal) {
|
|
776
|
+
active.push(trapFocus(dom, target));
|
|
777
|
+
active.push(containFocus(dom, target));
|
|
778
|
+
}
|
|
779
|
+
if (desc.closeOnEscape && desc.onClose !== void 0) {
|
|
780
|
+
active.push(onEscape(dom, target, desc.onClose));
|
|
781
|
+
}
|
|
782
|
+
} else {
|
|
783
|
+
teardown();
|
|
784
|
+
if (desc.restoreFocus && saved !== null) {
|
|
785
|
+
restoreFocus(dom, saved);
|
|
786
|
+
saved = null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
const unsub = openSig.subscribe(onOpenChange);
|
|
791
|
+
instance.trackCleanup(unsub);
|
|
792
|
+
instance.trackCleanup(teardown);
|
|
793
|
+
if (openSig.peek() === true) onOpenChange(true);
|
|
794
|
+
}
|
|
708
795
|
|
|
709
796
|
// src/renderer.ts
|
|
710
797
|
import { BrowserDOMAdapter } from "@streetui/dom";
|
|
@@ -810,6 +897,25 @@ function hydrateNode(ctx, graphNode, domNode, path) {
|
|
|
810
897
|
wireReactiveList(ctx, graphNode, instance, domNode);
|
|
811
898
|
return instance;
|
|
812
899
|
}
|
|
900
|
+
case "portal": {
|
|
901
|
+
const instance = new NodeInstance(graphNode, domNode);
|
|
902
|
+
ctx.instances.set(graphNode.id, instance);
|
|
903
|
+
const body = dom.body();
|
|
904
|
+
let target = domNode;
|
|
905
|
+
if (body !== null) {
|
|
906
|
+
const portalContainer = dom.createElement("div");
|
|
907
|
+
dom.setAttribute(portalContainer, "data-streetui-portal-container", "");
|
|
908
|
+
for (const child of dom.childNodes(domNode)) {
|
|
909
|
+
dom.appendChild(portalContainer, child);
|
|
910
|
+
}
|
|
911
|
+
dom.appendChild(body, portalContainer);
|
|
912
|
+
instance.trackCleanup(() => dom.removeChild(body, portalContainer));
|
|
913
|
+
target = portalContainer;
|
|
914
|
+
}
|
|
915
|
+
hydrateChildren(ctx, graphNode, instance, target, path);
|
|
916
|
+
wireOverlayBehavior(ctx, graphNode, instance, target);
|
|
917
|
+
return instance;
|
|
918
|
+
}
|
|
813
919
|
default: {
|
|
814
920
|
const instance = new NodeInstance(graphNode, domNode);
|
|
815
921
|
ctx.instances.set(graphNode.id, instance);
|
|
@@ -1025,11 +1131,64 @@ function readState(dom, root) {
|
|
|
1025
1131
|
}
|
|
1026
1132
|
|
|
1027
1133
|
// src/ssr.ts
|
|
1134
|
+
import { ServerDOMAdapter as ServerDOMAdapter2 } from "@streetui/dom";
|
|
1135
|
+
|
|
1136
|
+
// src/static-ssr-plan.ts
|
|
1137
|
+
import { analyzeGraph } from "@streetui/compiler/diagnostics";
|
|
1028
1138
|
import { ServerDOMAdapter } from "@streetui/dom";
|
|
1139
|
+
function collectMaximalStaticRoots(graph) {
|
|
1140
|
+
const analysis = analyzeGraph(graph);
|
|
1141
|
+
const roots = [];
|
|
1142
|
+
const walk = (node) => {
|
|
1143
|
+
if (node.type !== "application") {
|
|
1144
|
+
const a = analysis.nodes.get(node.id);
|
|
1145
|
+
if (a !== void 0 && a.isStaticSubtree) {
|
|
1146
|
+
roots.push(node);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
for (const child of node.children) walk(child);
|
|
1151
|
+
};
|
|
1152
|
+
walk(graph.root);
|
|
1153
|
+
return roots;
|
|
1154
|
+
}
|
|
1155
|
+
function serializeStaticSubtree(dom, graph, root) {
|
|
1156
|
+
const container = dom.createElement("div");
|
|
1157
|
+
const ctx = createRenderContext(dom, graph, container);
|
|
1158
|
+
const instance = mountNode(ctx, root, container);
|
|
1159
|
+
const html = dom.serializeInner(container);
|
|
1160
|
+
instance.dispose();
|
|
1161
|
+
ctx.instances.clear();
|
|
1162
|
+
return html;
|
|
1163
|
+
}
|
|
1164
|
+
function buildStaticSSRPlan(compiled) {
|
|
1165
|
+
const graph = compiled.graph;
|
|
1166
|
+
const roots = collectMaximalStaticRoots(graph);
|
|
1167
|
+
const plan = /* @__PURE__ */ new Map();
|
|
1168
|
+
if (roots.length === 0) return plan;
|
|
1169
|
+
const dom = new ServerDOMAdapter();
|
|
1170
|
+
for (const root of roots) {
|
|
1171
|
+
plan.set(root.id, serializeStaticSubtree(dom, graph, root));
|
|
1172
|
+
}
|
|
1173
|
+
return plan;
|
|
1174
|
+
}
|
|
1175
|
+
var PLAN_CACHE = /* @__PURE__ */ new WeakMap();
|
|
1176
|
+
function getStaticSSRPlan(compiled) {
|
|
1177
|
+
let plan = PLAN_CACHE.get(compiled);
|
|
1178
|
+
if (plan === void 0) {
|
|
1179
|
+
plan = buildStaticSSRPlan(compiled);
|
|
1180
|
+
PLAN_CACHE.set(compiled, plan);
|
|
1181
|
+
}
|
|
1182
|
+
return plan;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/ssr.ts
|
|
1029
1186
|
function renderToString(compiled, options = {}) {
|
|
1030
|
-
const dom = options.domAdapter ?? new
|
|
1187
|
+
const dom = options.domAdapter ?? new ServerDOMAdapter2();
|
|
1188
|
+
const plan = options.staticPlan === null ? void 0 : options.staticPlan ?? getStaticSSRPlan(compiled);
|
|
1189
|
+
const staticHTML = plan !== void 0 && plan.size > 0 ? plan : void 0;
|
|
1031
1190
|
const container = dom.createElement("div");
|
|
1032
|
-
const ctx = createRenderContext(dom, compiled.graph, container);
|
|
1191
|
+
const ctx = createRenderContext(dom, compiled.graph, container, void 0, staticHTML);
|
|
1033
1192
|
const rootInstance = mountGraph(ctx);
|
|
1034
1193
|
const html = dom.serializeInner(container);
|
|
1035
1194
|
rootInstance.dispose();
|
|
@@ -1064,6 +1223,7 @@ export {
|
|
|
1064
1223
|
serializeState,
|
|
1065
1224
|
textUpdate,
|
|
1066
1225
|
wireEvents,
|
|
1226
|
+
wireOverlayBehavior,
|
|
1067
1227
|
wireReactiveList,
|
|
1068
1228
|
wireSignalBindings
|
|
1069
1229
|
};
|