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.
Files changed (58) hide show
  1. package/dist/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
  2. package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
  3. package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
  4. package/dist/cli.js +462 -496
  5. package/dist/cloudflare.cjs +11 -11
  6. package/dist/cloudflare.js +3 -3
  7. package/dist/index.d.cts +8 -4
  8. package/dist/index.d.ts +8 -4
  9. package/dist/lambda.cjs +11 -11
  10. package/dist/lambda.js +7 -7
  11. package/dist/loader.cjs +90 -54
  12. package/dist/slim-react/index.cjs +13 -13
  13. package/dist/slim-react/index.js +2 -2
  14. package/dist/slim-react/jsx-runtime.cjs +2 -4
  15. package/dist/slim-react/jsx-runtime.js +1 -1
  16. package/dist/ssr-render-worker.js +174 -161
  17. package/dist/ssr-watch.js +40 -74
  18. package/package.json +8 -10
  19. package/cli-lib.ts +0 -676
  20. package/cli.ts +0 -36
  21. package/index.ts +0 -17
  22. package/src/build.ts +0 -805
  23. package/src/cloudflare.ts +0 -140
  24. package/src/index.tsx +0 -41
  25. package/src/lambda.ts +0 -287
  26. package/src/slim-react/context.ts +0 -55
  27. package/src/slim-react/dispatcher.ts +0 -87
  28. package/src/slim-react/hooks.ts +0 -137
  29. package/src/slim-react/index.ts +0 -232
  30. package/src/slim-react/jsx-runtime.ts +0 -7
  31. package/src/slim-react/jsx.ts +0 -53
  32. package/src/slim-react/render.ts +0 -1101
  33. package/src/slim-react/renderContext.ts +0 -294
  34. package/src/slim-react/types.ts +0 -33
  35. package/src/source/context.ts +0 -113
  36. package/src/source/graphiql.ts +0 -101
  37. package/src/source/inference.ts +0 -260
  38. package/src/source/runner.ts +0 -138
  39. package/src/source/store.ts +0 -50
  40. package/src/ssr-render-worker.ts +0 -116
  41. package/src/ssr-watch.ts +0 -62
  42. package/src/static.ts +0 -109
  43. package/src/types/global.d.ts +0 -5
  44. package/src/types/hadars.ts +0 -350
  45. package/src/utils/Head.tsx +0 -462
  46. package/src/utils/clientScript.tsx +0 -71
  47. package/src/utils/cookies.ts +0 -16
  48. package/src/utils/loader.ts +0 -335
  49. package/src/utils/proxyHandler.tsx +0 -104
  50. package/src/utils/request.tsx +0 -9
  51. package/src/utils/response.tsx +0 -141
  52. package/src/utils/rspack.ts +0 -467
  53. package/src/utils/runtime.ts +0 -19
  54. package/src/utils/serve.ts +0 -155
  55. package/src/utils/ssrHandler.ts +0 -239
  56. package/src/utils/staticFile.ts +0 -43
  57. package/src/utils/template.html +0 -11
  58. package/src/utils/upgradeRequest.tsx +0 -19
@@ -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> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#x27;' };
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> = { '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' };
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 };