hadars 0.4.1 → 0.4.2
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/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
- package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
- package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
- package/dist/cli.js +462 -496
- package/dist/cloudflare.cjs +11 -11
- package/dist/cloudflare.js +3 -3
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/lambda.cjs +11 -11
- package/dist/lambda.js +7 -7
- package/dist/loader.cjs +90 -54
- package/dist/slim-react/index.cjs +13 -13
- package/dist/slim-react/index.js +2 -2
- package/dist/slim-react/jsx-runtime.cjs +2 -4
- package/dist/slim-react/jsx-runtime.js +1 -1
- package/dist/ssr-render-worker.js +174 -161
- package/dist/ssr-watch.js +40 -74
- package/package.json +8 -10
- package/cli-lib.ts +0 -676
- package/cli.ts +0 -36
- package/index.ts +0 -17
- package/src/build.ts +0 -805
- package/src/cloudflare.ts +0 -140
- package/src/index.tsx +0 -41
- package/src/lambda.ts +0 -287
- package/src/slim-react/context.ts +0 -55
- package/src/slim-react/dispatcher.ts +0 -87
- package/src/slim-react/hooks.ts +0 -137
- package/src/slim-react/index.ts +0 -232
- package/src/slim-react/jsx-runtime.ts +0 -7
- package/src/slim-react/jsx.ts +0 -53
- package/src/slim-react/render.ts +0 -1101
- package/src/slim-react/renderContext.ts +0 -294
- package/src/slim-react/types.ts +0 -33
- package/src/source/context.ts +0 -113
- package/src/source/graphiql.ts +0 -101
- package/src/source/inference.ts +0 -260
- package/src/source/runner.ts +0 -138
- package/src/source/store.ts +0 -50
- package/src/ssr-render-worker.ts +0 -116
- package/src/ssr-watch.ts +0 -62
- package/src/static.ts +0 -109
- package/src/types/global.d.ts +0 -5
- package/src/types/hadars.ts +0 -350
- package/src/utils/Head.tsx +0 -462
- package/src/utils/clientScript.tsx +0 -71
- package/src/utils/cookies.ts +0 -16
- package/src/utils/loader.ts +0 -335
- package/src/utils/proxyHandler.tsx +0 -104
- package/src/utils/request.tsx +0 -9
- package/src/utils/response.tsx +0 -141
- package/src/utils/rspack.ts +0 -467
- package/src/utils/runtime.ts +0 -19
- package/src/utils/serve.ts +0 -155
- package/src/utils/ssrHandler.ts +0 -239
- package/src/utils/staticFile.ts +0 -43
- package/src/utils/template.html +0 -11
- package/src/utils/upgradeRequest.tsx +0 -19
package/src/slim-react/render.ts
DELETED
|
@@ -1,1101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Streaming SSR renderer with Suspense support.
|
|
3
|
-
*
|
|
4
|
-
* `renderToStream` walks the virtual-node tree produced by jsx() /
|
|
5
|
-
* createElement() and writes HTML chunks into a ReadableStream.
|
|
6
|
-
*
|
|
7
|
-
* When it meets a <Suspense> boundary it:
|
|
8
|
-
* 1. Tries to render the children into a temporary buffer.
|
|
9
|
-
* 2. If a child throws a Promise (React Suspense protocol) it
|
|
10
|
-
* awaits the promise, then retries from step 1.
|
|
11
|
-
* 3. Once successful, the buffer is flushed to the real stream.
|
|
12
|
-
*
|
|
13
|
-
* The net effect is that the stream **pauses** at Suspense boundaries
|
|
14
|
-
* until the async data is ready, then continues – exactly as requested.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
SLIM_ELEMENT,
|
|
19
|
-
REACT19_ELEMENT,
|
|
20
|
-
FRAGMENT_TYPE,
|
|
21
|
-
SUSPENSE_TYPE,
|
|
22
|
-
type SlimElement,
|
|
23
|
-
type SlimNode,
|
|
24
|
-
} from "./types";
|
|
25
|
-
import {
|
|
26
|
-
resetRenderState,
|
|
27
|
-
pushTreeContext,
|
|
28
|
-
popTreeContext,
|
|
29
|
-
pushComponentScope,
|
|
30
|
-
popComponentScope,
|
|
31
|
-
componentCalledUseId,
|
|
32
|
-
snapshotContext,
|
|
33
|
-
restoreContext,
|
|
34
|
-
pushContextValue,
|
|
35
|
-
popContextValue,
|
|
36
|
-
getContextValue,
|
|
37
|
-
swapContextMap,
|
|
38
|
-
captureMap,
|
|
39
|
-
captureUnsuspend,
|
|
40
|
-
restoreUnsuspend,
|
|
41
|
-
type ContextSnapshot,
|
|
42
|
-
} from "./renderContext";
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Capture all three concurrent-render globals in one call.
|
|
46
|
-
* Must be called immediately before every `await` and the returned token
|
|
47
|
-
* passed to restoreRenderCtx immediately after resuming — just like the
|
|
48
|
-
* individual captureMap / captureUnsuspend calls they replace.
|
|
49
|
-
*/
|
|
50
|
-
function captureRenderCtx(): { m: ReturnType<typeof captureMap>; u: unknown; t: ContextSnapshot } {
|
|
51
|
-
return { m: captureMap(), u: captureUnsuspend(), t: snapshotContext() };
|
|
52
|
-
}
|
|
53
|
-
function restoreRenderCtx(ctx: ReturnType<typeof captureRenderCtx>): void {
|
|
54
|
-
swapContextMap(ctx.m); restoreUnsuspend(ctx.u); restoreContext(ctx.t);
|
|
55
|
-
}
|
|
56
|
-
import { installDispatcher, restoreDispatcher } from "./dispatcher";
|
|
57
|
-
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
// HTML helpers
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
|
|
62
|
-
const VOID_ELEMENTS = new Set([
|
|
63
|
-
"area",
|
|
64
|
-
"base",
|
|
65
|
-
"br",
|
|
66
|
-
"col",
|
|
67
|
-
"embed",
|
|
68
|
-
"hr",
|
|
69
|
-
"img",
|
|
70
|
-
"input",
|
|
71
|
-
"link",
|
|
72
|
-
"meta",
|
|
73
|
-
"param",
|
|
74
|
-
"source",
|
|
75
|
-
"track",
|
|
76
|
-
"wbr",
|
|
77
|
-
]);
|
|
78
|
-
|
|
79
|
-
const HTML_ESC: Record<string, string> = { '&': '&', '<': '<', '>': '>', "'": ''' };
|
|
80
|
-
const HTML_ESC_RE = /[&<>']/;
|
|
81
|
-
function escapeHtml(str: string): string {
|
|
82
|
-
// Fast path: avoid regex replace + callback allocation when there's nothing to escape.
|
|
83
|
-
if (!HTML_ESC_RE.test(str)) return str;
|
|
84
|
-
return str.replace(/[&<>']/g, c => HTML_ESC[c]!);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const ATTR_ESC: Record<string, string> = { '&': '&', '"': '"', '<': '<', '>': '>' };
|
|
88
|
-
const ATTR_ESC_RE = /[&"<>]/;
|
|
89
|
-
function escapeAttr(str: string): string {
|
|
90
|
-
if (!ATTR_ESC_RE.test(str)) return str;
|
|
91
|
-
return str.replace(/[&"<>]/g, c => ATTR_ESC[c]!);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* CSS properties that accept plain numbers without a `px` suffix.
|
|
96
|
-
* Matches React's internal unitless-number list so SSR output agrees with
|
|
97
|
-
* client-side React during hydration.
|
|
98
|
-
*/
|
|
99
|
-
const UNITLESS_CSS = new Set([
|
|
100
|
-
'animationIterationCount', 'aspectRatio', 'borderImageOutset', 'borderImageSlice',
|
|
101
|
-
'borderImageWidth', 'boxFlex', 'boxFlexGroup', 'boxOrdinalGroup', 'columnCount',
|
|
102
|
-
'columns', 'flex', 'flexGrow', 'flexPositive', 'flexShrink', 'flexNegative',
|
|
103
|
-
'flexOrder', 'gridArea', 'gridRow', 'gridRowEnd', 'gridRowSpan', 'gridRowStart',
|
|
104
|
-
'gridColumn', 'gridColumnEnd', 'gridColumnSpan', 'gridColumnStart', 'fontWeight',
|
|
105
|
-
'lineClamp', 'lineHeight', 'opacity', 'order', 'orphans', 'scale', 'tabSize',
|
|
106
|
-
'widows', 'zIndex', 'zoom', 'fillOpacity', 'floodOpacity', 'stopOpacity',
|
|
107
|
-
'strokeDasharray', 'strokeDashoffset', 'strokeMiterlimit', 'strokeOpacity',
|
|
108
|
-
'strokeWidth',
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
/** Intern camelCase → kebab-case CSS property name conversions. */
|
|
112
|
-
const _cssKeyCache = new Map<string, string>();
|
|
113
|
-
function styleObjectToString(style: Record<string, any>): string {
|
|
114
|
-
let result = '';
|
|
115
|
-
for (const key in style) {
|
|
116
|
-
const value = style[key];
|
|
117
|
-
// Skip null, undefined and boolean values (React behaviour).
|
|
118
|
-
if (value == null || typeof value === 'boolean') continue;
|
|
119
|
-
if (result) result += ';';
|
|
120
|
-
// camelCase → kebab-case, cached to avoid repeated regex per render.
|
|
121
|
-
let cssKey = _cssKeyCache.get(key);
|
|
122
|
-
if (cssKey === undefined) {
|
|
123
|
-
cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
124
|
-
_cssKeyCache.set(key, cssKey);
|
|
125
|
-
}
|
|
126
|
-
// Append 'px' for numeric values on non-unitless properties (React behaviour).
|
|
127
|
-
if (typeof value === 'number' && value !== 0 && !UNITLESS_CSS.has(key)) {
|
|
128
|
-
result += cssKey + ':' + value + 'px';
|
|
129
|
-
} else {
|
|
130
|
-
result += cssKey + ':' + value;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// SVG attribute name mappings
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* React camelCase prop → actual SVG attribute.
|
|
142
|
-
* Covers the most commonly used SVG attributes.
|
|
143
|
-
*/
|
|
144
|
-
const SVG_ATTR_MAP: Record<string, string> = {
|
|
145
|
-
// Presentation / geometry
|
|
146
|
-
accentHeight: "accent-height",
|
|
147
|
-
alignmentBaseline: "alignment-baseline",
|
|
148
|
-
arabicForm: "arabic-form",
|
|
149
|
-
baselineShift: "baseline-shift",
|
|
150
|
-
capHeight: "cap-height",
|
|
151
|
-
clipPath: "clip-path",
|
|
152
|
-
clipRule: "clip-rule",
|
|
153
|
-
colorInterpolation: "color-interpolation",
|
|
154
|
-
colorInterpolationFilters: "color-interpolation-filters",
|
|
155
|
-
colorProfile: "color-profile",
|
|
156
|
-
dominantBaseline: "dominant-baseline",
|
|
157
|
-
enableBackground: "enable-background",
|
|
158
|
-
fillOpacity: "fill-opacity",
|
|
159
|
-
fillRule: "fill-rule",
|
|
160
|
-
floodColor: "flood-color",
|
|
161
|
-
floodOpacity: "flood-opacity",
|
|
162
|
-
fontFamily: "font-family",
|
|
163
|
-
fontSize: "font-size",
|
|
164
|
-
fontSizeAdjust: "font-size-adjust",
|
|
165
|
-
fontStretch: "font-stretch",
|
|
166
|
-
fontStyle: "font-style",
|
|
167
|
-
fontVariant: "font-variant",
|
|
168
|
-
fontWeight: "font-weight",
|
|
169
|
-
glyphName: "glyph-name",
|
|
170
|
-
glyphOrientationHorizontal: "glyph-orientation-horizontal",
|
|
171
|
-
glyphOrientationVertical: "glyph-orientation-vertical",
|
|
172
|
-
horizAdvX: "horiz-adv-x",
|
|
173
|
-
horizOriginX: "horiz-origin-x",
|
|
174
|
-
imageRendering: "image-rendering",
|
|
175
|
-
letterSpacing: "letter-spacing",
|
|
176
|
-
lightingColor: "lighting-color",
|
|
177
|
-
markerEnd: "marker-end",
|
|
178
|
-
markerMid: "marker-mid",
|
|
179
|
-
markerStart: "marker-start",
|
|
180
|
-
overlinePosition: "overline-position",
|
|
181
|
-
overlineThickness: "overline-thickness",
|
|
182
|
-
paintOrder: "paint-order",
|
|
183
|
-
panose1: "panose-1",
|
|
184
|
-
pointerEvents: "pointer-events",
|
|
185
|
-
renderingIntent: "rendering-intent",
|
|
186
|
-
shapeRendering: "shape-rendering",
|
|
187
|
-
stopColor: "stop-color",
|
|
188
|
-
stopOpacity: "stop-opacity",
|
|
189
|
-
strikethroughPosition: "strikethrough-position",
|
|
190
|
-
strikethroughThickness: "strikethrough-thickness",
|
|
191
|
-
strokeDasharray: "stroke-dasharray",
|
|
192
|
-
strokeDashoffset: "stroke-dashoffset",
|
|
193
|
-
strokeLinecap: "stroke-linecap",
|
|
194
|
-
strokeLinejoin: "stroke-linejoin",
|
|
195
|
-
strokeMiterlimit: "stroke-miterlimit",
|
|
196
|
-
strokeOpacity: "stroke-opacity",
|
|
197
|
-
strokeWidth: "stroke-width",
|
|
198
|
-
textAnchor: "text-anchor",
|
|
199
|
-
textDecoration: "text-decoration",
|
|
200
|
-
textRendering: "text-rendering",
|
|
201
|
-
underlinePosition: "underline-position",
|
|
202
|
-
underlineThickness: "underline-thickness",
|
|
203
|
-
unicodeBidi: "unicode-bidi",
|
|
204
|
-
unicodeRange: "unicode-range",
|
|
205
|
-
unitsPerEm: "units-per-em",
|
|
206
|
-
vAlphabetic: "v-alphabetic",
|
|
207
|
-
vHanging: "v-hanging",
|
|
208
|
-
vIdeographic: "v-ideographic",
|
|
209
|
-
vMathematical: "v-mathematical",
|
|
210
|
-
vertAdvY: "vert-adv-y",
|
|
211
|
-
vertOriginX: "vert-origin-x",
|
|
212
|
-
vertOriginY: "vert-origin-y",
|
|
213
|
-
wordSpacing: "word-spacing",
|
|
214
|
-
writingMode: "writing-mode",
|
|
215
|
-
xHeight: "x-height",
|
|
216
|
-
|
|
217
|
-
// Namespace-prefixed
|
|
218
|
-
xlinkActuate: "xlink:actuate",
|
|
219
|
-
xlinkArcrole: "xlink:arcrole",
|
|
220
|
-
xlinkHref: "xlink:href",
|
|
221
|
-
xlinkRole: "xlink:role",
|
|
222
|
-
xlinkShow: "xlink:show",
|
|
223
|
-
xlinkTitle: "xlink:title",
|
|
224
|
-
xlinkType: "xlink:type",
|
|
225
|
-
xmlBase: "xml:base",
|
|
226
|
-
xmlLang: "xml:lang",
|
|
227
|
-
xmlSpace: "xml:space",
|
|
228
|
-
xmlns: "xmlns",
|
|
229
|
-
xmlnsXlink: "xmlns:xlink",
|
|
230
|
-
|
|
231
|
-
// Filter / lighting
|
|
232
|
-
baseFrequency: "baseFrequency",
|
|
233
|
-
colorInterpolation_filters: "color-interpolation-filters",
|
|
234
|
-
diffuseConstant: "diffuseConstant",
|
|
235
|
-
edgeMode: "edgeMode",
|
|
236
|
-
filterUnits: "filterUnits",
|
|
237
|
-
gradientTransform: "gradientTransform",
|
|
238
|
-
gradientUnits: "gradientUnits",
|
|
239
|
-
kernelMatrix: "kernelMatrix",
|
|
240
|
-
kernelUnitLength: "kernelUnitLength",
|
|
241
|
-
lengthAdjust: "lengthAdjust",
|
|
242
|
-
limitingConeAngle: "limitingConeAngle",
|
|
243
|
-
markerHeight: "markerHeight",
|
|
244
|
-
markerWidth: "markerWidth",
|
|
245
|
-
maskContentUnits: "maskContentUnits",
|
|
246
|
-
maskUnits: "maskUnits",
|
|
247
|
-
numOctaves: "numOctaves",
|
|
248
|
-
pathLength: "pathLength",
|
|
249
|
-
patternContentUnits: "patternContentUnits",
|
|
250
|
-
patternTransform: "patternTransform",
|
|
251
|
-
patternUnits: "patternUnits",
|
|
252
|
-
pointsAtX: "pointsAtX",
|
|
253
|
-
pointsAtY: "pointsAtY",
|
|
254
|
-
pointsAtZ: "pointsAtZ",
|
|
255
|
-
preserveAspectRatio: "preserveAspectRatio",
|
|
256
|
-
primitiveUnits: "primitiveUnits",
|
|
257
|
-
refX: "refX",
|
|
258
|
-
refY: "refY",
|
|
259
|
-
repeatCount: "repeatCount",
|
|
260
|
-
repeatDur: "repeatDur",
|
|
261
|
-
specularConstant: "specularConstant",
|
|
262
|
-
specularExponent: "specularExponent",
|
|
263
|
-
spreadMethod: "spreadMethod",
|
|
264
|
-
startOffset: "startOffset",
|
|
265
|
-
stdDeviation: "stdDeviation",
|
|
266
|
-
stitchTiles: "stitchTiles",
|
|
267
|
-
surfaceScale: "surfaceScale",
|
|
268
|
-
systemLanguage: "systemLanguage",
|
|
269
|
-
tableValues: "tableValues",
|
|
270
|
-
targetX: "targetX",
|
|
271
|
-
targetY: "targetY",
|
|
272
|
-
textLength: "textLength",
|
|
273
|
-
viewBox: "viewBox",
|
|
274
|
-
xChannelSelector: "xChannelSelector",
|
|
275
|
-
yChannelSelector: "yChannelSelector",
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// Pre-allocated skip-sets for special host elements that strip certain props
|
|
279
|
-
// before delegating to writeAttributes. Module-level so they are created once.
|
|
280
|
-
const TEXTAREA_SKIP_PROPS = new Set(["value", "defaultValue", "children"]);
|
|
281
|
-
const SELECT_SKIP_PROPS = new Set(["value", "defaultValue"]);
|
|
282
|
-
|
|
283
|
-
// Internal React props that must never be serialised as HTML attributes.
|
|
284
|
-
// A Set lookup (one hash probe) replaces six sequential string comparisons
|
|
285
|
-
// for every attribute on every element — the hottest path in the renderer.
|
|
286
|
-
const INTERNAL_PROPS = new Set([
|
|
287
|
-
"children", "key", "ref",
|
|
288
|
-
"dangerouslySetInnerHTML",
|
|
289
|
-
"suppressHydrationWarning",
|
|
290
|
-
"suppressContentEditableWarning",
|
|
291
|
-
]);
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Write element attributes directly into the writer, skipping the
|
|
295
|
-
* intermediate `attrs` string that `renderAttributes` used to return.
|
|
296
|
-
* Eliminates one heap string allocation per element.
|
|
297
|
-
*
|
|
298
|
-
* @param skip - Optional set of prop names to exclude (used by textarea/select).
|
|
299
|
-
*/
|
|
300
|
-
function writeAttributes(writer: Writer, props: Record<string, any>, isSvg: boolean, skip?: ReadonlySet<string>): void {
|
|
301
|
-
for (const key in props) {
|
|
302
|
-
if (skip !== undefined && skip.has(key)) continue;
|
|
303
|
-
const value = props[key];
|
|
304
|
-
// Skip internal / non-attribute props — one hash probe replaces 6 comparisons.
|
|
305
|
-
if (INTERNAL_PROPS.has(key)) continue;
|
|
306
|
-
// Skip event handlers (onClick, onChange, …) — use charCodeAt for speed.
|
|
307
|
-
if (
|
|
308
|
-
key.length > 2 &&
|
|
309
|
-
key.charCodeAt(0) === 111 /*o*/ &&
|
|
310
|
-
key.charCodeAt(1) === 110 /*n*/ &&
|
|
311
|
-
key.charCodeAt(2) >= 65 && key.charCodeAt(2) <= 90 /*A-Z*/
|
|
312
|
-
) continue;
|
|
313
|
-
|
|
314
|
-
// Prop-name mapping
|
|
315
|
-
let attrName: string;
|
|
316
|
-
if (isSvg && key in SVG_ATTR_MAP) {
|
|
317
|
-
attrName = SVG_ATTR_MAP[key]!;
|
|
318
|
-
} else {
|
|
319
|
-
attrName =
|
|
320
|
-
key === "className" ? "class"
|
|
321
|
-
: key === "htmlFor" ? "for"
|
|
322
|
-
: key === "tabIndex" ? "tabindex"
|
|
323
|
-
: key === "defaultValue" ? "value"
|
|
324
|
-
: key === "defaultChecked" ? "checked"
|
|
325
|
-
: key;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (value === false || value == null) {
|
|
329
|
-
if (value === false && (attrName.charCodeAt(0) === 97 /*a*/ && attrName.startsWith("aria-") ||
|
|
330
|
-
attrName.charCodeAt(0) === 100 /*d*/ && attrName.startsWith("data-"))) {
|
|
331
|
-
writer.write(` ${attrName}="false"`);
|
|
332
|
-
}
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (value === true) {
|
|
336
|
-
if (attrName.charCodeAt(0) === 97 /*a*/ && attrName.startsWith("aria-") ||
|
|
337
|
-
attrName.charCodeAt(0) === 100 /*d*/ && attrName.startsWith("data-")) {
|
|
338
|
-
writer.write(` ${attrName}="true"`);
|
|
339
|
-
} else {
|
|
340
|
-
writer.write(` ${attrName}=""`);
|
|
341
|
-
}
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
if (key === "style" && typeof value === "object") {
|
|
345
|
-
const styleStr = styleObjectToString(value);
|
|
346
|
-
if (styleStr) writer.write(` style="${escapeAttr(styleStr)}"`);
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
writer.write(` ${attrName}="${escapeAttr(typeof value === 'string' ? value : String(value))}"`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
// Writer abstraction (stream vs buffer)
|
|
355
|
-
// ---------------------------------------------------------------------------
|
|
356
|
-
|
|
357
|
-
interface Writer {
|
|
358
|
-
/** Write raw HTML markup. Resets lastWasText to false. */
|
|
359
|
-
write(chunk: string): void;
|
|
360
|
-
/** Write escaped text content. Sets lastWasText to true. */
|
|
361
|
-
text(s: string): void;
|
|
362
|
-
/** True if the last thing written was a text node (not markup). */
|
|
363
|
-
lastWasText: boolean;
|
|
364
|
-
/**
|
|
365
|
-
* Optional: encode and flush any internal string buffer downstream.
|
|
366
|
-
* Called at natural streaming boundaries (Suspense completions, end of render).
|
|
367
|
-
* Writers that don't buffer (e.g. BufferWriter, NullWriter) leave this undefined.
|
|
368
|
-
*/
|
|
369
|
-
flush?(): void;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
class BufferWriter implements Writer {
|
|
373
|
-
data = "";
|
|
374
|
-
lastWasText = false;
|
|
375
|
-
write(chunk: string) {
|
|
376
|
-
this.data += chunk;
|
|
377
|
-
this.lastWasText = false;
|
|
378
|
-
}
|
|
379
|
-
text(s: string) {
|
|
380
|
-
this.data += s;
|
|
381
|
-
this.lastWasText = true;
|
|
382
|
-
}
|
|
383
|
-
/** Flush accumulated output into a parent writer and reset. */
|
|
384
|
-
flushTo(target: Writer) {
|
|
385
|
-
if (!this.data) return; // nothing buffered — preserve target's lastWasText
|
|
386
|
-
// Single write call — the entire buffered string in one shot.
|
|
387
|
-
if (target instanceof BufferWriter) {
|
|
388
|
-
target.data += this.data;
|
|
389
|
-
} else {
|
|
390
|
-
target.write(this.data);
|
|
391
|
-
}
|
|
392
|
-
target.lastWasText = this.lastWasText;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ---------------------------------------------------------------------------
|
|
397
|
-
// Core recursive renderer (sync-first design)
|
|
398
|
-
//
|
|
399
|
-
// `renderNode` is synchronous for the fast path (plain HTML elements,
|
|
400
|
-
// text, fragments, pure function components). It only returns a
|
|
401
|
-
// Promise when something actually async happens (Suspense throw,
|
|
402
|
-
// async component). This eliminates thousands of unnecessary
|
|
403
|
-
// microtask bounces for a typical component tree.
|
|
404
|
-
// ---------------------------------------------------------------------------
|
|
405
|
-
|
|
406
|
-
type MaybePromise = void | Promise<void>;
|
|
407
|
-
|
|
408
|
-
function renderNode(
|
|
409
|
-
node: SlimNode,
|
|
410
|
-
writer: Writer,
|
|
411
|
-
isSvg = false,
|
|
412
|
-
): MaybePromise {
|
|
413
|
-
// --- primitives / nullish ---
|
|
414
|
-
if (node == null || typeof node === "boolean") return;
|
|
415
|
-
if (typeof node === "string") {
|
|
416
|
-
writer.text(escapeHtml(node));
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (typeof node === "number") {
|
|
420
|
-
writer.text(String(node));
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// --- arrays ---
|
|
425
|
-
if (Array.isArray(node)) {
|
|
426
|
-
return renderChildArray(node, writer, isSvg);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// At this point node is guaranteed to be a non-null object — null/boolean/
|
|
430
|
-
// string/number/Array are all handled above. The iterable and $$typeof
|
|
431
|
-
// branches no longer need to re-test typeof/null.
|
|
432
|
-
const obj = node as any;
|
|
433
|
-
|
|
434
|
-
// --- iterables (Set, generator, …) ---
|
|
435
|
-
if (Symbol.iterator in obj && !("$$typeof" in obj)) {
|
|
436
|
-
return renderChildArray(Array.from(obj as Iterable<SlimNode>), writer, isSvg);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// --- SlimElement (accepts both the classic and React 19 transitional symbols) ---
|
|
440
|
-
if ("$$typeof" in obj) {
|
|
441
|
-
const elType = obj["$$typeof"] as symbol;
|
|
442
|
-
if (elType !== SLIM_ELEMENT && elType !== REACT19_ELEMENT) return;
|
|
443
|
-
const element = node as SlimElement;
|
|
444
|
-
const { type, props } = element;
|
|
445
|
-
|
|
446
|
-
// Fragment
|
|
447
|
-
if (type === FRAGMENT_TYPE) {
|
|
448
|
-
return renderChildren(props.children, writer, isSvg);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Suspense – always async
|
|
452
|
-
if (type === SUSPENSE_TYPE) {
|
|
453
|
-
return renderSuspense(props, writer, isSvg);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// HTML / SVG element — most common; check string before function to
|
|
457
|
-
// hit the branch earlier for the majority of nodes.
|
|
458
|
-
if (typeof type === "string") {
|
|
459
|
-
return renderHostElement(type, props, writer, isSvg);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Function / class component
|
|
463
|
-
if (typeof type === "function") {
|
|
464
|
-
return renderComponent(type, props, writer, isSvg);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Object component wrappers: React.memo, React.forwardRef,
|
|
468
|
-
// Context.Provider (React 19: the context IS the provider),
|
|
469
|
-
// Context.Consumer — all identified by their own $$typeof.
|
|
470
|
-
if (typeof type === "object" && type !== null) {
|
|
471
|
-
return renderComponent(type as unknown as Function, props, writer, isSvg);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Recursively clone `<option>` / `<optgroup>` nodes inside a `<select>` tree,
|
|
478
|
-
* stamping `selected` on options whose value is in `selectedValues`.
|
|
479
|
-
* Handles both single-select and multi-select (defaultValue array).
|
|
480
|
-
*/
|
|
481
|
-
function markSelectedOptionsMulti(children: SlimNode, selectedValues: Set<string>): SlimNode {
|
|
482
|
-
if (children == null || typeof children === "boolean") return children;
|
|
483
|
-
if (typeof children === "string" || typeof children === "number") return children;
|
|
484
|
-
if (Array.isArray(children)) {
|
|
485
|
-
return children.map((c) => markSelectedOptionsMulti(c, selectedValues));
|
|
486
|
-
}
|
|
487
|
-
if (
|
|
488
|
-
typeof children === "object" &&
|
|
489
|
-
"$$typeof" in children
|
|
490
|
-
) {
|
|
491
|
-
const elType = (children as any)["$$typeof"] as symbol;
|
|
492
|
-
if (elType !== SLIM_ELEMENT && elType !== REACT19_ELEMENT) return children;
|
|
493
|
-
const el = children as SlimElement;
|
|
494
|
-
if (el.type === "option") {
|
|
495
|
-
// Option value falls back to its text children if no value prop.
|
|
496
|
-
const optValue = el.props.value !== undefined ? el.props.value : el.props.children;
|
|
497
|
-
const isSelected = selectedValues.has(String(optValue));
|
|
498
|
-
return { ...el, props: { ...el.props, selected: isSelected || undefined } };
|
|
499
|
-
}
|
|
500
|
-
if (el.type === "optgroup" || el.type === FRAGMENT_TYPE) {
|
|
501
|
-
const newChildren = markSelectedOptionsMulti(el.props.children, selectedValues);
|
|
502
|
-
return { ...el, props: { ...el.props, children: newChildren } };
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return children;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/** Render a host (HTML/SVG) element. Sync when children are sync. */
|
|
509
|
-
function renderHostElement(
|
|
510
|
-
tag: string,
|
|
511
|
-
props: Record<string, any>,
|
|
512
|
-
writer: Writer,
|
|
513
|
-
isSvg: boolean,
|
|
514
|
-
): MaybePromise {
|
|
515
|
-
const childSvg = isSvg || tag === "svg";
|
|
516
|
-
|
|
517
|
-
// ── <textarea> ────────────────────────────────────────────────────────────
|
|
518
|
-
if (tag === "textarea") {
|
|
519
|
-
const textContent = props.value ?? props.defaultValue ?? props.children ?? "";
|
|
520
|
-
writer.write("<textarea");
|
|
521
|
-
writeAttributes(writer, props, false, TEXTAREA_SKIP_PROPS);
|
|
522
|
-
writer.write(">");
|
|
523
|
-
writer.text(escapeHtml(String(textContent)));
|
|
524
|
-
writer.write("</textarea>");
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// ── <select> ──────────────────────────────────────────────────────────────
|
|
529
|
-
// React never emits a `value` attribute on <select>; instead it marks the
|
|
530
|
-
// matching <option> as `selected`.
|
|
531
|
-
if (tag === "select") {
|
|
532
|
-
const selectedValue = props.value ?? props.defaultValue;
|
|
533
|
-
writer.write("<select");
|
|
534
|
-
writeAttributes(writer, props, false, SELECT_SKIP_PROPS);
|
|
535
|
-
writer.write(">");
|
|
536
|
-
// Normalise selectedValue to a Set of strings to handle both single values
|
|
537
|
-
// and arrays (multi-select with defaultValue={['a','b']}).
|
|
538
|
-
const selectedSet: Set<string> | null =
|
|
539
|
-
selectedValue == null
|
|
540
|
-
? null
|
|
541
|
-
: Array.isArray(selectedValue)
|
|
542
|
-
? new Set((selectedValue as unknown[]).map(String))
|
|
543
|
-
: new Set([String(selectedValue)]);
|
|
544
|
-
const patchedChildren =
|
|
545
|
-
selectedSet != null
|
|
546
|
-
? markSelectedOptionsMulti(props.children, selectedSet)
|
|
547
|
-
: props.children;
|
|
548
|
-
const inner = renderChildren(patchedChildren, writer, false);
|
|
549
|
-
if (inner && typeof (inner as any).then === "function") {
|
|
550
|
-
return (inner as Promise<void>).then(() => { writer.write("</select>"); });
|
|
551
|
-
}
|
|
552
|
-
writer.write("</select>");
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// React 19 does not inject xmlns on <svg> — browsers handle SVG namespaces
|
|
557
|
-
// automatically for inline HTML5 SVG, so we match React's behaviour.
|
|
558
|
-
writer.write(`<${tag}`);
|
|
559
|
-
writeAttributes(writer, props, childSvg);
|
|
560
|
-
|
|
561
|
-
// Void elements are self-closing (matching React's output format).
|
|
562
|
-
if (VOID_ELEMENTS.has(tag)) {
|
|
563
|
-
writer.write("/>");
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
writer.write(">");
|
|
568
|
-
const childContext = tag === "foreignObject" ? false : childSvg;
|
|
569
|
-
|
|
570
|
-
let inner: MaybePromise = undefined;
|
|
571
|
-
if (props.dangerouslySetInnerHTML) {
|
|
572
|
-
writer.write(props.dangerouslySetInnerHTML.__html);
|
|
573
|
-
} else {
|
|
574
|
-
inner = renderChildren(props.children, writer, childContext);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (inner && typeof (inner as any).then === "function") {
|
|
578
|
-
return (inner as Promise<void>).then(() => { writer.write(`</${tag}>`); });
|
|
579
|
-
}
|
|
580
|
-
writer.write(`</${tag}>`);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// React special $$typeof symbols for memo, forwardRef, context/provider/consumer, lazy
|
|
584
|
-
const REACT_MEMO = Symbol.for("react.memo");
|
|
585
|
-
const REACT_FORWARD_REF = Symbol.for("react.forward_ref");
|
|
586
|
-
const REACT_PROVIDER = Symbol.for("react.provider"); // React 18 Provider object
|
|
587
|
-
const REACT_CONTEXT = Symbol.for("react.context"); // React 19: context IS provider
|
|
588
|
-
const REACT_CONSUMER = Symbol.for("react.consumer"); // React 19 Consumer object
|
|
589
|
-
const REACT_LAZY = Symbol.for("react.lazy"); // React.lazy()
|
|
590
|
-
|
|
591
|
-
// Sentinel thrown by renderComponent when a component exceeds its per-boundary
|
|
592
|
-
// suspension retry limit. Caught by renderSuspense to trigger fallback rendering.
|
|
593
|
-
// Using a unique object (not a subclass) keeps the check a fast reference equality.
|
|
594
|
-
const SUSPENSE_RETRY_LIMIT: unique symbol = Symbol("SuspenseRetryLimit");
|
|
595
|
-
const MAX_COMPONENT_SUSPENSE_RETRIES = 25;
|
|
596
|
-
|
|
597
|
-
/** React 19 `use()` protocol — patch a thrown promise with status tracking so
|
|
598
|
-
* that `use(promise)` can return the resolved value synchronously on retry. */
|
|
599
|
-
function patchPromiseStatus(p: Promise<unknown>): void {
|
|
600
|
-
const w = p as Promise<unknown> & { status?: string; value?: unknown; reason?: unknown };
|
|
601
|
-
if (w.status) return; // already tracked (e.g. React.lazy payload)
|
|
602
|
-
w.status = "pending";
|
|
603
|
-
w.then(
|
|
604
|
-
(v) => { w.status = "fulfilled"; w.value = v; },
|
|
605
|
-
(r) => { w.status = "rejected"; w.reason = r; },
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/** Render a function or class component. */
|
|
610
|
-
function renderComponent(
|
|
611
|
-
type: Function,
|
|
612
|
-
props: Record<string, any>,
|
|
613
|
-
writer: Writer,
|
|
614
|
-
isSvg: boolean,
|
|
615
|
-
_suspenseRetries = 0,
|
|
616
|
-
): MaybePromise {
|
|
617
|
-
// type is always a defined Function — the optional chain is never needed.
|
|
618
|
-
const typeOf = (type as any).$$typeof;
|
|
619
|
-
|
|
620
|
-
// React.memo — unwrap and re-render the inner type
|
|
621
|
-
if (typeOf === REACT_MEMO) {
|
|
622
|
-
return renderNode(
|
|
623
|
-
{ $$typeof: SLIM_ELEMENT, type: (type as any).type, props, key: null } as any,
|
|
624
|
-
writer, isSvg,
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// React.forwardRef — call the wrapped render function
|
|
629
|
-
if (typeOf === REACT_FORWARD_REF) {
|
|
630
|
-
return renderComponent((type as any).render, props, writer, isSvg);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// React.lazy — initialise via the _init/_payload protocol; may suspend.
|
|
634
|
-
if (typeOf === REACT_LAZY) {
|
|
635
|
-
// _init returns the resolved module (or throws a Promise/Error).
|
|
636
|
-
let resolved: any;
|
|
637
|
-
try {
|
|
638
|
-
resolved = (type as any)._init((type as any)._payload);
|
|
639
|
-
} catch (e) {
|
|
640
|
-
// Module not yet loaded — treat as a component-level suspension.
|
|
641
|
-
if (e && typeof (e as any).then === "function") {
|
|
642
|
-
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
643
|
-
patchPromiseStatus(e as Promise<unknown>);
|
|
644
|
-
const rctx = captureRenderCtx();
|
|
645
|
-
return (e as Promise<unknown>).then(() => {
|
|
646
|
-
restoreRenderCtx(rctx);
|
|
647
|
-
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
throw e;
|
|
651
|
-
}
|
|
652
|
-
// The module may export `.default` or be the component directly.
|
|
653
|
-
const LazyComp = resolved?.default ?? resolved;
|
|
654
|
-
return renderComponent(LazyComp, props, writer, isSvg);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// React.Consumer (React 19) — call the children render prop with the current value
|
|
658
|
-
if (typeOf === REACT_CONSUMER) {
|
|
659
|
-
const ctx = (type as any)._context;
|
|
660
|
-
const value = ctx ? getContextValue(ctx) : undefined;
|
|
661
|
-
const result: SlimNode =
|
|
662
|
-
typeof props.children === "function" ? props.children(value) : null;
|
|
663
|
-
const savedScope = pushComponentScope();
|
|
664
|
-
const finish = () => popComponentScope(savedScope);
|
|
665
|
-
const r = renderNode(result, writer, isSvg);
|
|
666
|
-
if (r && typeof (r as any).then === "function") {
|
|
667
|
-
return (r as Promise<void>).then(finish);
|
|
668
|
-
}
|
|
669
|
-
finish();
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Provider detection:
|
|
674
|
-
// slim-react: Provider function has `_context` property
|
|
675
|
-
// React 18: Provider object has $$typeof === react.provider and ._context
|
|
676
|
-
// React 19: Context object itself is the provider ($$typeof === react.context + value prop)
|
|
677
|
-
const isProvider =
|
|
678
|
-
"_context" in type ||
|
|
679
|
-
typeOf === REACT_PROVIDER ||
|
|
680
|
-
(typeOf === REACT_CONTEXT && "value" in props);
|
|
681
|
-
|
|
682
|
-
let prevCtxValue: any;
|
|
683
|
-
let ctx: any;
|
|
684
|
-
|
|
685
|
-
if (isProvider) {
|
|
686
|
-
// Resolve the actual context object from any provider variant
|
|
687
|
-
ctx = (type as any)._context ?? type;
|
|
688
|
-
prevCtxValue = pushContextValue(ctx, props.value);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Each component gets a fresh local-ID counter (for multiple useId calls).
|
|
692
|
-
const savedScope = pushComponentScope();
|
|
693
|
-
|
|
694
|
-
// For React 19 Provider (context object IS the provider — not callable), just
|
|
695
|
-
// render children directly; the context value was already pushed above.
|
|
696
|
-
if (isProvider && typeof type !== "function") {
|
|
697
|
-
const finish = () => {
|
|
698
|
-
popComponentScope(savedScope);
|
|
699
|
-
popContextValue(ctx, prevCtxValue);
|
|
700
|
-
};
|
|
701
|
-
const r = renderChildren(props.children, writer, isSvg);
|
|
702
|
-
if (r && typeof (r as any).then === "function") {
|
|
703
|
-
const rctx = captureRenderCtx();
|
|
704
|
-
return (r as Promise<void>).then(
|
|
705
|
-
() => { restoreRenderCtx(rctx); finish(); },
|
|
706
|
-
(e) => { restoreRenderCtx(rctx); finish(); throw e; },
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
finish();
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
let result: SlimNode;
|
|
714
|
-
const prevDispatcher = installDispatcher();
|
|
715
|
-
try {
|
|
716
|
-
if (type.prototype && typeof type.prototype.render === "function") {
|
|
717
|
-
const instance = new (type as any)(props);
|
|
718
|
-
// Call getDerivedStateFromProps if defined, matching React's behaviour.
|
|
719
|
-
if (typeof (type as any).getDerivedStateFromProps === "function") {
|
|
720
|
-
const derived = (type as any).getDerivedStateFromProps(props, instance.state ?? {});
|
|
721
|
-
if (derived != null) instance.state = { ...(instance.state ?? {}), ...derived };
|
|
722
|
-
}
|
|
723
|
-
result = instance.render();
|
|
724
|
-
} else {
|
|
725
|
-
result = type(props);
|
|
726
|
-
}
|
|
727
|
-
} catch (e) {
|
|
728
|
-
restoreDispatcher(prevDispatcher);
|
|
729
|
-
popComponentScope(savedScope);
|
|
730
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
731
|
-
// Suspense protocol: the component threw a Promise (e.g. useServerData without
|
|
732
|
-
// a <Suspense> wrapper). Context is fully restored at this point — dispatcher,
|
|
733
|
-
// component scope and context value are all popped back to pre-component state.
|
|
734
|
-
// Convert the throw into a returned Promise so the parent never sees a throw and
|
|
735
|
-
// no root restart is needed: we await the promise then retry ONLY this component.
|
|
736
|
-
if (e && typeof (e as any).then === "function") {
|
|
737
|
-
if (_suspenseRetries + 1 >= MAX_COMPONENT_SUSPENSE_RETRIES) throw SUSPENSE_RETRY_LIMIT;
|
|
738
|
-
patchPromiseStatus(e as Promise<unknown>);
|
|
739
|
-
const rctx = captureRenderCtx();
|
|
740
|
-
return (e as Promise<unknown>).then(() => {
|
|
741
|
-
restoreRenderCtx(rctx);
|
|
742
|
-
return renderComponent(type, props, writer, isSvg, _suspenseRetries + 1);
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
throw e;
|
|
746
|
-
}
|
|
747
|
-
restoreDispatcher(prevDispatcher);
|
|
748
|
-
|
|
749
|
-
// React 19 finishFunctionComponent: if the component called useId, push a
|
|
750
|
-
// tree-context slot for the component's OUTPUT children — matching React 19's
|
|
751
|
-
// `pushTreeContext(keyPath, 1, 0)` call inside finishFunctionComponent.
|
|
752
|
-
// This ensures that useId IDs produced by child components of a useId-calling
|
|
753
|
-
// component are tree-positioned identically to React's own renderer.
|
|
754
|
-
let savedIdTree: number | undefined;
|
|
755
|
-
if (!(result instanceof Promise) && componentCalledUseId()) {
|
|
756
|
-
savedIdTree = pushTreeContext(1, 0);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Async component
|
|
760
|
-
if (result instanceof Promise) {
|
|
761
|
-
const rctx = captureRenderCtx();
|
|
762
|
-
return result.then((resolved) => {
|
|
763
|
-
restoreRenderCtx(rctx);
|
|
764
|
-
// Check useId after the async body has finished executing.
|
|
765
|
-
let asyncSavedIdTree: number | undefined;
|
|
766
|
-
if (componentCalledUseId()) {
|
|
767
|
-
asyncSavedIdTree = pushTreeContext(1, 0);
|
|
768
|
-
}
|
|
769
|
-
const r = renderNode(resolved, writer, isSvg);
|
|
770
|
-
if (r && typeof (r as any).then === "function") {
|
|
771
|
-
const rctx2 = captureRenderCtx();
|
|
772
|
-
// Only allocate cleanup closures when actually going async.
|
|
773
|
-
return (r as Promise<void>).then(
|
|
774
|
-
() => {
|
|
775
|
-
restoreRenderCtx(rctx2);
|
|
776
|
-
if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
|
|
777
|
-
popComponentScope(savedScope);
|
|
778
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
779
|
-
},
|
|
780
|
-
(e) => {
|
|
781
|
-
restoreRenderCtx(rctx2);
|
|
782
|
-
if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
|
|
783
|
-
popComponentScope(savedScope);
|
|
784
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
785
|
-
throw e;
|
|
786
|
-
},
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
// Sync result from async component — inline cleanup.
|
|
790
|
-
if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
|
|
791
|
-
popComponentScope(savedScope);
|
|
792
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
793
|
-
}, (e) => {
|
|
794
|
-
restoreRenderCtx(rctx);
|
|
795
|
-
// savedIdTree is always undefined here (async component skips the push).
|
|
796
|
-
popComponentScope(savedScope);
|
|
797
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
798
|
-
throw e;
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const r = renderNode(result, writer, isSvg);
|
|
803
|
-
|
|
804
|
-
if (r && typeof (r as any).then === "function") {
|
|
805
|
-
const rctx = captureRenderCtx();
|
|
806
|
-
// Only allocate cleanup closures when actually going async.
|
|
807
|
-
return (r as Promise<void>).then(
|
|
808
|
-
() => {
|
|
809
|
-
restoreRenderCtx(rctx);
|
|
810
|
-
if (savedIdTree !== undefined) popTreeContext(savedIdTree);
|
|
811
|
-
popComponentScope(savedScope);
|
|
812
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
813
|
-
},
|
|
814
|
-
(e) => {
|
|
815
|
-
restoreRenderCtx(rctx);
|
|
816
|
-
if (savedIdTree !== undefined) popTreeContext(savedIdTree);
|
|
817
|
-
popComponentScope(savedScope);
|
|
818
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
819
|
-
throw e;
|
|
820
|
-
},
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
// Sync path — inline cleanup, no closure allocation.
|
|
824
|
-
if (savedIdTree !== undefined) popTreeContext(savedIdTree);
|
|
825
|
-
popComponentScope(savedScope);
|
|
826
|
-
if (isProvider) popContextValue(ctx, prevCtxValue);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/** Render an array of children, pushing tree-context for each child
|
|
830
|
-
* so that `useId` produces deterministic, position-based IDs. */
|
|
831
|
-
function renderChildArray(
|
|
832
|
-
children: SlimNode[],
|
|
833
|
-
writer: Writer,
|
|
834
|
-
isSvg: boolean,
|
|
835
|
-
): MaybePromise {
|
|
836
|
-
return renderChildArrayFrom(children, 0, writer, isSvg);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/** Core child-array loop. Used by both the initial call and async continuations. */
|
|
840
|
-
function renderChildArrayFrom(
|
|
841
|
-
children: SlimNode[],
|
|
842
|
-
startIndex: number,
|
|
843
|
-
writer: Writer,
|
|
844
|
-
isSvg: boolean,
|
|
845
|
-
): MaybePromise {
|
|
846
|
-
const totalChildren = children.length;
|
|
847
|
-
for (let i = startIndex; i < totalChildren; i++) {
|
|
848
|
-
// Inline isTextLike — avoids a function call on every child in every array.
|
|
849
|
-
const child = children[i];
|
|
850
|
-
if ((typeof child === "string" || typeof child === "number") && writer.lastWasText) {
|
|
851
|
-
writer.write("<!-- -->");
|
|
852
|
-
}
|
|
853
|
-
const savedTree = pushTreeContext(totalChildren, i);
|
|
854
|
-
const r = renderNode(child, writer, isSvg);
|
|
855
|
-
if (r && typeof (r as any).then === "function") {
|
|
856
|
-
const rctx = captureRenderCtx();
|
|
857
|
-
return (r as Promise<void>).then(() => {
|
|
858
|
-
restoreRenderCtx(rctx);
|
|
859
|
-
popTreeContext(savedTree);
|
|
860
|
-
return renderChildArrayFrom(children, i + 1, writer, isSvg);
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
popTreeContext(savedTree);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function renderChildren(
|
|
868
|
-
children: SlimNode,
|
|
869
|
-
writer: Writer,
|
|
870
|
-
isSvg = false,
|
|
871
|
-
): MaybePromise {
|
|
872
|
-
if (children == null) return;
|
|
873
|
-
if (Array.isArray(children)) {
|
|
874
|
-
return renderChildArray(children, writer, isSvg);
|
|
875
|
-
}
|
|
876
|
-
return renderNode(children, writer, isSvg);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ---------------------------------------------------------------------------
|
|
880
|
-
// Suspense boundary renderer
|
|
881
|
-
//
|
|
882
|
-
// Sibling Suspense boundaries within the same parent are resolved
|
|
883
|
-
// **in parallel**: we kick off all of them concurrently and stream
|
|
884
|
-
// their results in document order once each resolves.
|
|
885
|
-
// ---------------------------------------------------------------------------
|
|
886
|
-
|
|
887
|
-
async function renderSuspense(
|
|
888
|
-
props: Record<string, any>,
|
|
889
|
-
writer: Writer,
|
|
890
|
-
isSvg = false,
|
|
891
|
-
): Promise<void> {
|
|
892
|
-
const { children, fallback } = props;
|
|
893
|
-
// Snapshot tree-context so we can restore it if we need to render the fallback.
|
|
894
|
-
const snap = snapshotContext();
|
|
895
|
-
// Shallow-clone the context map so we can restore Provider values on fallback.
|
|
896
|
-
// Provider push/pop pairs inside the failed children may not complete
|
|
897
|
-
// symmetrically when SUSPENSE_RETRY_LIMIT is thrown. The clone is a shallow
|
|
898
|
-
// copy of a small Map (one entry per active Provider), so the cost is negligible.
|
|
899
|
-
const savedMap = captureMap();
|
|
900
|
-
const savedMapClone = savedMap ? new Map(savedMap) : null;
|
|
901
|
-
// Collect all output into a buffer so we can discard it if the boundary
|
|
902
|
-
// falls back to the loading state.
|
|
903
|
-
const buffer = new BufferWriter();
|
|
904
|
-
|
|
905
|
-
// Components handle their own Promise throws (see renderComponent catch block),
|
|
906
|
-
// so renderNode either resolves synchronously or returns a Promise — it never
|
|
907
|
-
// throws a Promise here. SUSPENSE_RETRY_LIMIT is thrown when a component
|
|
908
|
-
// exhausts its retry budget, signalling us to render the fallback instead.
|
|
909
|
-
try {
|
|
910
|
-
const r = renderNode(children, buffer, isSvg);
|
|
911
|
-
if (r && typeof (r as any).then === "function") {
|
|
912
|
-
const rctx = captureRenderCtx();
|
|
913
|
-
await r;
|
|
914
|
-
restoreRenderCtx(rctx);
|
|
915
|
-
}
|
|
916
|
-
// Success – wrap with React's Suspense boundary markers so hydrateRoot
|
|
917
|
-
// can locate the boundary in the DOM (<!--$--> … <!--/$-->).
|
|
918
|
-
writer.write("<!--$-->");
|
|
919
|
-
buffer.flushTo(writer);
|
|
920
|
-
writer.write("<!--/$-->");
|
|
921
|
-
// Tell a streaming writer it can encode and enqueue everything accumulated
|
|
922
|
-
// so far — this is a natural boundary where partial HTML is complete.
|
|
923
|
-
writer.flush?.();
|
|
924
|
-
} catch (error) {
|
|
925
|
-
if ((error as any) === SUSPENSE_RETRY_LIMIT) {
|
|
926
|
-
// A component inside this boundary exhausted its retry budget.
|
|
927
|
-
// Restore context to Suspense-entry state and render the fallback.
|
|
928
|
-
restoreContext(snap);
|
|
929
|
-
// Restore the context map to its pre-boundary state.
|
|
930
|
-
swapContextMap(savedMapClone);
|
|
931
|
-
writer.write("<!--$?-->");
|
|
932
|
-
if (fallback) {
|
|
933
|
-
const r = renderNode(fallback, writer, isSvg);
|
|
934
|
-
if (r && typeof (r as any).then === "function") {
|
|
935
|
-
const rctx = captureRenderCtx();
|
|
936
|
-
await r;
|
|
937
|
-
restoreRenderCtx(rctx);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
writer.write("<!--/$-->");
|
|
941
|
-
} else {
|
|
942
|
-
throw error;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// ---------------------------------------------------------------------------
|
|
948
|
-
// Public API
|
|
949
|
-
// ---------------------------------------------------------------------------
|
|
950
|
-
|
|
951
|
-
export interface RenderOptions {
|
|
952
|
-
/**
|
|
953
|
-
* Must match the `identifierPrefix` option passed to `hydrateRoot` on the
|
|
954
|
-
* client so that `useId()` generates identical IDs on server and client.
|
|
955
|
-
* Defaults to `""` (React's default).
|
|
956
|
-
*/
|
|
957
|
-
identifierPrefix?: string;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Module-level encoder — one instance shared across all renderToStream calls.
|
|
961
|
-
const _streamEncoder = new TextEncoder();
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Render a component tree to a `ReadableStream<Uint8Array>`.
|
|
965
|
-
*
|
|
966
|
-
* The stream pauses at `<Suspense>` boundaries until the suspended
|
|
967
|
-
* promise resolves, then continues writing HTML.
|
|
968
|
-
*/
|
|
969
|
-
export function renderToStream(
|
|
970
|
-
element: SlimNode,
|
|
971
|
-
options?: RenderOptions,
|
|
972
|
-
): ReadableStream<Uint8Array> {
|
|
973
|
-
const idPrefix = options?.identifierPrefix ?? "";
|
|
974
|
-
|
|
975
|
-
return new ReadableStream({
|
|
976
|
-
async start(controller) {
|
|
977
|
-
resetRenderState(idPrefix);
|
|
978
|
-
// Start with null — pushContextValue lazily creates the Map only if a
|
|
979
|
-
// Context.Provider is actually rendered, eliminating the allocation on
|
|
980
|
-
// the common (no-provider) path.
|
|
981
|
-
const prev = swapContextMap(null);
|
|
982
|
-
|
|
983
|
-
// Buffer writes into a string; only encode+enqueue in flush() so that
|
|
984
|
-
// a sync render produces one Uint8Array instead of thousands of tiny ones.
|
|
985
|
-
let _buf = "";
|
|
986
|
-
const writer: Writer = {
|
|
987
|
-
lastWasText: false,
|
|
988
|
-
write(chunk: string) { _buf += chunk; this.lastWasText = false; },
|
|
989
|
-
text(s: string) { _buf += s; this.lastWasText = true; },
|
|
990
|
-
flush() {
|
|
991
|
-
if (_buf.length > 0) {
|
|
992
|
-
controller.enqueue(_streamEncoder.encode(_buf));
|
|
993
|
-
_buf = "";
|
|
994
|
-
}
|
|
995
|
-
},
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
try {
|
|
999
|
-
const r = renderNode(element, writer);
|
|
1000
|
-
if (r && typeof (r as any).then === "function") {
|
|
1001
|
-
const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
|
|
1002
|
-
}
|
|
1003
|
-
writer.flush!(); // encode everything accumulated (sync renders: the whole page)
|
|
1004
|
-
controller.close();
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
controller.error(error);
|
|
1007
|
-
} finally {
|
|
1008
|
-
swapContextMap(prev);
|
|
1009
|
-
}
|
|
1010
|
-
},
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// ---------------------------------------------------------------------------
|
|
1015
|
-
// Preflight renderer
|
|
1016
|
-
// ---------------------------------------------------------------------------
|
|
1017
|
-
|
|
1018
|
-
/** A writer that discards all output — only side-effects (cache warming, head
|
|
1019
|
-
* population) are preserved. Used as the no-op sink for Pass 1. */
|
|
1020
|
-
const NULL_WRITER: Writer = {
|
|
1021
|
-
lastWasText: false,
|
|
1022
|
-
write(_c: string) {},
|
|
1023
|
-
text(_s: string) {},
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Pass-1 preflight render.
|
|
1028
|
-
*
|
|
1029
|
-
* Walks the component tree with a NullWriter (discards all HTML output) so
|
|
1030
|
-
* that all `useServerData` promises are resolved into the `__hadarsUnsuspend`
|
|
1031
|
-
* cache and all `context.head` mutations are applied.
|
|
1032
|
-
*
|
|
1033
|
-
* Components self-retry on suspension at the component level (see
|
|
1034
|
-
* `renderComponent` catch block), so a single tree walk is sufficient.
|
|
1035
|
-
*
|
|
1036
|
-
* Call this before `renderToString` / `renderToStream` to guarantee a
|
|
1037
|
-
* suspension-free, fully-synchronous second pass.
|
|
1038
|
-
*/
|
|
1039
|
-
export async function renderPreflight(
|
|
1040
|
-
element: SlimNode,
|
|
1041
|
-
options?: RenderOptions,
|
|
1042
|
-
): Promise<void> {
|
|
1043
|
-
const idPrefix = options?.identifierPrefix ?? "";
|
|
1044
|
-
// Start with null — pushContextValue lazily creates the Map only if a
|
|
1045
|
-
// Context.Provider is actually rendered.
|
|
1046
|
-
const prev = swapContextMap(null);
|
|
1047
|
-
try {
|
|
1048
|
-
resetRenderState(idPrefix);
|
|
1049
|
-
NULL_WRITER.lastWasText = false;
|
|
1050
|
-
// Components self-retry on suspension (see renderComponent catch block),
|
|
1051
|
-
// so a single pass is guaranteed to complete with all promises resolved.
|
|
1052
|
-
const r = renderNode(element, NULL_WRITER);
|
|
1053
|
-
if (r && typeof (r as any).then === "function") {
|
|
1054
|
-
const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
|
|
1055
|
-
}
|
|
1056
|
-
} finally {
|
|
1057
|
-
swapContextMap(prev);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
/**
|
|
1062
|
-
* Render a component tree to a complete HTML string.
|
|
1063
|
-
*
|
|
1064
|
-
* Components self-retry on suspension at the component level (see
|
|
1065
|
-
* `renderComponent` catch block), so a single tree walk is sufficient
|
|
1066
|
-
* even when `useServerData` or similar hooks are used without an explicit
|
|
1067
|
-
* `<Suspense>` wrapper.
|
|
1068
|
-
*/
|
|
1069
|
-
export async function renderToString(
|
|
1070
|
-
element: SlimNode,
|
|
1071
|
-
options?: RenderOptions,
|
|
1072
|
-
): Promise<string> {
|
|
1073
|
-
const idPrefix = options?.identifierPrefix ?? "";
|
|
1074
|
-
// Start with null — pushContextValue lazily creates the Map only if a
|
|
1075
|
-
// Context.Provider is actually rendered.
|
|
1076
|
-
const prev = swapContextMap(null);
|
|
1077
|
-
// Use a single mutable string rather than a chunks array + join() —
|
|
1078
|
-
// JSC/V8 use rope strings for += that are flattened once at return time,
|
|
1079
|
-
// avoiding all the array bookkeeping and the final allocation at join().
|
|
1080
|
-
let output = "";
|
|
1081
|
-
const writer: Writer = {
|
|
1082
|
-
lastWasText: false,
|
|
1083
|
-
write(c) { output += c; this.lastWasText = false; },
|
|
1084
|
-
text(s) { output += s; this.lastWasText = true; },
|
|
1085
|
-
};
|
|
1086
|
-
try {
|
|
1087
|
-
resetRenderState(idPrefix);
|
|
1088
|
-
// Components self-retry on suspension (see renderComponent catch block),
|
|
1089
|
-
// so a single pass is guaranteed to complete with all promises resolved.
|
|
1090
|
-
const r = renderNode(element, writer);
|
|
1091
|
-
if (r && typeof (r as any).then === "function") {
|
|
1092
|
-
const rctx = captureRenderCtx(); await r; restoreRenderCtx(rctx);
|
|
1093
|
-
}
|
|
1094
|
-
return output;
|
|
1095
|
-
} finally {
|
|
1096
|
-
swapContextMap(prev);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/** Alias matching React 18+ server API naming. */
|
|
1101
|
-
export { renderToStream as renderToReadableStream };
|