@reckona/mreact-server 0.0.81 → 0.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,10 @@
1
- import { Fragment, Suspense as ReactCompatSuspense, createElement, isValidElement, } from "@reckona/mreact-compat";
2
- import { isDangerousHtmlAttribute, isDangerousHtmlOptIn, isUnsafeMetaRefreshContent, isUnsafeUrlAttribute, } from "./url-safety.js";
3
- import { escapeHtmlText as escapeHtml } from "@reckona/mreact-shared/html-escape";
4
- import { createStreamingBufferSink } from "./buffer-sink.js";
1
+ import { Suspense as ReactCompatSuspense, createElement, } from "@reckona/mreact-compat";
5
2
  export { Fragment } from "@reckona/mreact-compat";
6
3
  export { CLIENT_REFERENCE_TYPE, SERVER_REFERENCE_TYPE, createClientReference, createFlightClientManifest, createServerReference, createServerActionHandler, fromReactFlightRows, getReactFlightProtocolCoverage, isClientReference, isServerReference, mergeReactFlightRows, renderFlightPreloadLinks, renderFlightResponseScript, renderToFlightResponse, stringifyFlightResponse, toReactFlightRows, } from "./flight.js";
7
- const outOfOrderBoundaryInstances = new WeakMap();
8
- const streamQueuedChunkSoftLimitBytes = 1024 * 1024;
4
+ export { createStringSink, } from "./sink.js";
5
+ export { renderToReadableStream, } from "./stream.js";
6
+ export { reactSuspenseRevealExternalScript, renderAsyncBoundary, renderHydrationBoundary, renderOutOfOrderBoundary, renderOutOfOrderReorderScript, renderReactSuspenseBoundary, renderReactSuspenseClientRenderBoundary, renderReactSuspenseOutOfOrderBoundary, } from "./boundary.js";
7
+ export { createEventHydrationManifest, html, renderEventHydrationManifest, renderScriptAsset, renderSsrState, renderToString, serializeSsrState, } from "./html-helpers.js";
9
8
  export function Suspense(props) {
10
9
  const config = {};
11
10
  if (props.fallback !== undefined) {
@@ -13,792 +12,4 @@ export function Suspense(props) {
13
12
  }
14
13
  return createElement(ReactCompatSuspense, config, props.children);
15
14
  }
16
- export function createStringSink(options = {}) {
17
- // Default to "concat" — V8 rope flattening yields 2-6x throughput over
18
- // `Array#join("")` across all measured fixture sizes (see
19
- // docs/benchmarks/2026-05-12-server-sink-strategy.md). "array-join" stays
20
- // available as opt-in for scenarios that need lower peak memory.
21
- const requestedStrategy = options.strategy ?? "concat";
22
- const arrayJoinThreshold = options.arrayJoinThreshold ?? 256;
23
- const deferredTasks = [];
24
- let strategy = requestedStrategy === "auto"
25
- ? "concat"
26
- : requestedStrategy;
27
- let writeCount = 0;
28
- let text = "";
29
- const chunks = [];
30
- const switchConcatToArrayJoin = () => {
31
- if (strategy !== "concat") {
32
- return;
33
- }
34
- if (text !== "") {
35
- chunks.push(text);
36
- text = "";
37
- }
38
- strategy = "array-join";
39
- };
40
- return {
41
- append(chunk) {
42
- writeCount += 1;
43
- if (requestedStrategy === "auto" && strategy === "concat" && writeCount > arrayJoinThreshold) {
44
- switchConcatToArrayJoin();
45
- }
46
- if (strategy === "concat") {
47
- text += chunk;
48
- return;
49
- }
50
- chunks.push(chunk);
51
- },
52
- bufferStrategy() {
53
- return strategy;
54
- },
55
- defer(task) {
56
- deferredTasks.push(task);
57
- },
58
- async drain() {
59
- await Promise.all(deferredTasks);
60
- },
61
- toString() {
62
- if (strategy === "concat") {
63
- return text;
64
- }
65
- return chunks.join("");
66
- },
67
- };
68
- }
69
- export async function renderAsyncBoundary(sink, value, render, options = {}) {
70
- try {
71
- const resolved = await value;
72
- await sink.backpressure?.();
73
- await render(sink, resolved);
74
- if (options.hydrationAwaitId !== undefined) {
75
- appendAwaitHydrationData(sink, options.hydrationAwaitId, resolved);
76
- }
77
- }
78
- catch (error) {
79
- if (options.catch === undefined) {
80
- throw error;
81
- }
82
- await sink.backpressure?.();
83
- await options.catch(sink, error);
84
- }
85
- }
86
- // Threshold for `<Await>` payload size warnings (UTF-8 byte length of
87
- // JSON-serialized representation). 100KB warn / 1MB error follow the
88
- // "you're sending a lot of data" hint pattern used by other frameworks.
89
- const AWAIT_PAYLOAD_WARN_BYTES = 100 * 1024;
90
- const AWAIT_PAYLOAD_ERROR_BYTES = 1024 * 1024;
91
- function isProductionMode() {
92
- // `process` may not exist in cross-runtime environments (Cloudflare/Deno).
93
- // The server tsconfig does not include `@types/node`, so we look it up
94
- // through `globalThis` with a minimal local typing.
95
- const globalProcess = globalThis
96
- .process;
97
- try {
98
- return globalProcess?.env?.["NODE_ENV"] === "production";
99
- }
100
- catch {
101
- return false;
102
- }
103
- }
104
- function appendAwaitHydrationData(sink, awaitId, resolved) {
105
- const serialized = serializeAwaitHydrationValue(resolved, awaitId);
106
- if (serialized === undefined) {
107
- return;
108
- }
109
- const idLiteral = JSON.stringify(awaitId).replaceAll("<", "\\u003c");
110
- sink.append(`<script data-mreact-await=${idLiteral}>(self.__mreactAwaitData||(self.__mreactAwaitData={}))[${idLiteral}]={value:${serialized}}</script>`);
111
- }
112
- // Emits a single console.warn in dev when `resolved` contains shapes the
113
- // wire format will silently drop or coerce (Date / Map / Set / RegExp /
114
- // class instance / function / Symbol / nested non-POJO).
115
- function warnIfNonSerializableAwaitValue(value, awaitId) {
116
- if (isProductionMode()) {
117
- return;
118
- }
119
- if (!containsNonSerializableSurface(value)) {
120
- return;
121
- }
122
- console.warn(`[mreact] <Await value={...}> for "${awaitId}" includes non-serializable ` +
123
- `data (Date / Map / Set / RegExp / class instance / function / Symbol). ` +
124
- `The wire format uses JSON.stringify, so the client-side renderer may ` +
125
- `receive a different shape after the JSON round-trip. Convert to plain ` +
126
- `JSON-serializable data before it reaches <Await>. See ` +
127
- `https://github.com/t-k/mreact#streaming-loading-and-await.`);
128
- }
129
- function containsNonSerializableSurface(value) {
130
- if (value === null || value === undefined || typeof value !== "object") {
131
- return false;
132
- }
133
- if (value instanceof Date || value instanceof Map || value instanceof Set || value instanceof RegExp) {
134
- return true;
135
- }
136
- if (Array.isArray(value)) {
137
- return value.some((entry) => containsNonSerializableSurface(entry));
138
- }
139
- const proto = Object.getPrototypeOf(value);
140
- if (proto !== Object.prototype && proto !== null) {
141
- return true;
142
- }
143
- for (const entry of Object.values(value)) {
144
- if (containsNonSerializableSurface(entry)) {
145
- return true;
146
- }
147
- }
148
- return false;
149
- }
150
- function reportAwaitPayloadSize(json, awaitId) {
151
- const byteLength = typeof TextEncoder !== "undefined" ? new TextEncoder().encode(json).length : json.length;
152
- if (byteLength > AWAIT_PAYLOAD_ERROR_BYTES) {
153
- console.error(`[mreact] <Await> payload for "${awaitId}" is ${(byteLength / 1024 / 1024).toFixed(2)}MB, ` +
154
- `exceeding the 1MB error threshold. Large payloads inflate HTML response size and ` +
155
- `client memory pressure. Stream the data via loader or split into smaller boundaries.`);
156
- return;
157
- }
158
- if (byteLength > AWAIT_PAYLOAD_WARN_BYTES) {
159
- console.warn(`[mreact] large await payload for "${awaitId}": ${(byteLength / 1024).toFixed(1)}KB ` +
160
- `(over the 100KB warning threshold). Consider streaming the data via loader or ` +
161
- `splitting into smaller boundaries.`);
162
- }
163
- }
164
- function serializeAwaitHydrationValue(value, awaitId) {
165
- try {
166
- const json = JSON.stringify(value);
167
- if (json === undefined) {
168
- return undefined;
169
- }
170
- warnIfNonSerializableAwaitValue(value, awaitId);
171
- reportAwaitPayloadSize(json, awaitId);
172
- // Defuse `</script>` and runaway control characters so the data can be
173
- // safely embedded inside a `<script>` element.
174
- return json
175
- .replaceAll("<", "\\u003c")
176
- .replaceAll("
", "\\u2028")
177
- .replaceAll("
", "\\u2029");
178
- }
179
- catch {
180
- return undefined;
181
- }
182
- }
183
- function inheritBackpressure(childSink, parentSink) {
184
- if (parentSink.backpressure === undefined) {
185
- return childSink;
186
- }
187
- return {
188
- ...childSink,
189
- backpressure: parentSink.backpressure,
190
- };
191
- }
192
- export function renderOutOfOrderBoundary(sink, id, value, render, options = {}) {
193
- const boundaryId = nextOutOfOrderBoundaryInstanceId(sink, id);
194
- const placeholderSink = createStringSink();
195
- void options.placeholder?.(placeholderSink);
196
- const placeholderTag = normalizeOutOfOrderPlaceholderTag(options.placeholderTag);
197
- const hydrationStart = options.hydration === true ? `<!--mreact-h:start:${encodeURIComponent(boundaryId)}-->` : "";
198
- const hydrationEnd = options.hydration === true ? `<!--mreact-h:end:${encodeURIComponent(boundaryId)}-->` : "";
199
- sink.append(`${hydrationStart}<${placeholderTag} data-mreact-oob-placeholder="${escapeAttribute(boundaryId)}">${placeholderSink.toString()}</${placeholderTag}>${hydrationEnd}`);
200
- const task = renderOutOfOrderFragment(sink, boundaryId, value, render, options);
201
- if (sink.defer === undefined) {
202
- void task;
203
- return;
204
- }
205
- sink.defer(task);
206
- }
207
- function normalizeOutOfOrderPlaceholderTag(tag) {
208
- if (typeof tag !== "string") {
209
- return "span";
210
- }
211
- const normalized = tag.trim().toLowerCase();
212
- return /^[a-z][a-z0-9-]*$/.test(normalized) ? normalized : "span";
213
- }
214
- async function renderOutOfOrderFragment(sink, id, value, render, options) {
215
- const fragmentSink = inheritBackpressure(createStringSink(), sink);
216
- let resolvedValue;
217
- let hasResolvedValue = false;
218
- await renderAsyncBoundary(fragmentSink, value, async (childSink, resolved) => {
219
- resolvedValue = resolved;
220
- hasResolvedValue = true;
221
- await render(childSink, resolved);
222
- }, options.catch === undefined ? {} : { catch: options.catch });
223
- await fragmentSink.drain();
224
- sink.append(`<template data-mreact-oob-fragment="${escapeAttribute(id)}">${fragmentSink.toString()}</template>`);
225
- if (hasResolvedValue && options.hydrationAwaitId !== undefined) {
226
- appendAwaitHydrationData(sink, options.hydrationAwaitId, resolvedValue);
227
- }
228
- }
229
- export function renderOutOfOrderReorderScript(sink, options = {}) {
230
- const nonceAttribute = options.nonce === undefined ? "" : ` nonce="${escapeAttribute(options.nonce)}"`;
231
- if (options.src !== undefined) {
232
- sink.append(`<script data-mreact-oob-reorder${nonceAttribute} src="${escapeAttribute(options.src)}"></script>`);
233
- return;
234
- }
235
- sink.append(`<script data-mreact-oob-reorder${nonceAttribute}>${outOfOrderReorderScript}</script>`);
236
- }
237
- export function renderReactSuspenseBoundary(sink, render) {
238
- sink.append("<!--$-->");
239
- const result = render(sink);
240
- if (isPromiseLike(result)) {
241
- return result.then(() => {
242
- sink.append("<!--/$-->");
243
- });
244
- }
245
- sink.append("<!--/$-->");
246
- }
247
- export function renderReactSuspenseOutOfOrderBoundary(sink, boundaryId, segmentId, value, render, options = {}) {
248
- const actualBoundaryId = nextOutOfOrderBoundaryInstanceId(sink, boundaryId);
249
- const suffix = actualBoundaryId.slice(boundaryId.length);
250
- const actualSegmentId = `${segmentId}${suffix}`;
251
- const fallbackSink = createStringSink();
252
- void options.fallback?.(fallbackSink);
253
- sink.append(`<!--$?--><template id="${escapeAttribute(actualBoundaryId)}"></template>${fallbackSink.toString()}<!--/$-->`);
254
- const task = renderReactSuspenseSegment(sink, actualBoundaryId, actualSegmentId, value, render, options);
255
- if (sink.defer === undefined) {
256
- void task;
257
- return;
258
- }
259
- sink.defer(task);
260
- }
261
- function nextOutOfOrderBoundaryInstanceId(sink, id) {
262
- let instances = outOfOrderBoundaryInstances.get(sink);
263
- if (instances === undefined) {
264
- instances = new Map();
265
- outOfOrderBoundaryInstances.set(sink, instances);
266
- }
267
- const count = instances.get(id) ?? 0;
268
- instances.set(id, count + 1);
269
- return count === 0 ? id : `${id}-${count.toString(36)}`;
270
- }
271
- export function renderReactSuspenseClientRenderBoundary(sink, fallback, options = {}) {
272
- sink.append(`<!--$!--><template${renderReactSuspenseErrorAttributes(options)}></template>`);
273
- const result = fallback(sink);
274
- if (isPromiseLike(result)) {
275
- return result.then(() => {
276
- sink.append("<!--/$-->");
277
- });
278
- }
279
- sink.append("<!--/$-->");
280
- }
281
- async function renderReactSuspenseSegment(sink, boundaryId, segmentId, value, render, options) {
282
- const segmentSink = inheritBackpressure(createStringSink(), sink);
283
- await renderAsyncBoundary(segmentSink, value, render, options.catch === undefined ? {} : { catch: options.catch });
284
- sink.append(`<div hidden id="${escapeAttribute(segmentId)}">${segmentSink.toString()}</div>${renderReactSuspenseRevealScript(boundaryId, segmentId, options)}`);
285
- }
286
- function renderReactSuspenseErrorAttributes(options) {
287
- const message = options.message === undefined ? "" : ` data-msg="${escapeAttribute(options.message)}"`;
288
- const stack = options.stack === undefined ? "" : ` data-stck="${escapeAttribute(options.stack)}"`;
289
- return `${message}${stack}`;
290
- }
291
- function renderReactSuspenseRevealScript(boundaryId, segmentId, options = {}) {
292
- if (options.src !== undefined) {
293
- return `<script data-mreact-react-suspense-reveal${renderNonceAttribute(options.nonce)} src="${escapeAttribute(options.src)}" data-boundary-id="${escapeAttribute(boundaryId)}" data-segment-id="${escapeAttribute(segmentId)}"></script>`;
294
- }
295
- return `<script${renderNonceAttribute(options.nonce)}>${reactSuspenseRevealScriptBody};$RC(${serializeScriptJson(boundaryId)},${serializeScriptJson(segmentId)})</script>`;
296
- }
297
- export function renderHydrationBoundary(sink, id, render) {
298
- const markerId = encodeURIComponent(id);
299
- sink.append(`<!--mreact-h:start:${markerId}-->`);
300
- const result = render(sink);
301
- if (isPromiseLike(result)) {
302
- return result.then(() => {
303
- sink.append(`<!--mreact-h:end:${markerId}-->`);
304
- });
305
- }
306
- sink.append(`<!--mreact-h:end:${markerId}-->`);
307
- }
308
- export function serializeSsrState(value) {
309
- return serializeScriptJson(value);
310
- }
311
- function serializeScriptJson(value) {
312
- return JSON.stringify(value)
313
- .replaceAll("<", "\\u003c")
314
- .replaceAll("\u2028", "\\u2028")
315
- .replaceAll("\u2029", "\\u2029");
316
- }
317
- export function renderSsrState(sink, value, options = {}) {
318
- sink.append(`<script type="application/json" data-mreact-ssr-state${renderNonceAttribute(options.nonce)}>${serializeSsrState(value)}</script>`);
319
- }
320
- export function createEventHydrationManifest(events) {
321
- return {
322
- version: 1,
323
- events: events.map((event) => ({ ...event })),
324
- };
325
- }
326
- export function renderEventHydrationManifest(sink, manifest, options = {}) {
327
- sink.append(`<script type="application/json" data-mreact-event-manifest${renderNonceAttribute(options.nonce)}>${serializeSsrState(manifest)}</script>`);
328
- }
329
- export function renderScriptAsset(sink, options) {
330
- const integrityAttribute = options.integrity === undefined ? "" : ` integrity="${escapeAttribute(options.integrity)}"`;
331
- const crossOrigin = options.integrity === undefined ? options.crossOrigin : (options.crossOrigin ?? "anonymous");
332
- const crossOriginAttribute = crossOrigin === undefined ? "" : ` crossorigin="${escapeAttribute(crossOrigin)}"`;
333
- sink.append(`<script src="${escapeAttribute(options.src)}"${renderNonceAttribute(options.nonce)}${integrityAttribute}${crossOriginAttribute}></script>`);
334
- }
335
- export function html(node, options = {}) {
336
- const headers = new Headers(options.headers);
337
- const responseOptions = { headers };
338
- if (!headers.has("content-type")) {
339
- headers.set("content-type", "text/html; charset=utf-8");
340
- }
341
- if (options.status !== undefined) {
342
- responseOptions.status = options.status;
343
- }
344
- if (options.statusText !== undefined) {
345
- responseOptions.statusText = options.statusText;
346
- }
347
- return new Response(renderToReadableStream((sink) => {
348
- const state = { suspenseId: 0 };
349
- return appendReactNode(sink, node, state);
350
- }), responseOptions);
351
- }
352
- export async function renderToString(render) {
353
- const sink = createStringSink();
354
- await render(sink);
355
- await sink.drain();
356
- return sink.toString();
357
- }
358
- function appendReactNode(sink, node, state) {
359
- if (isPromiseLikeNode(node)) {
360
- return node.then((resolved) => appendReactNode(sink, resolved, state));
361
- }
362
- if (node === null || node === undefined || typeof node === "boolean") {
363
- return;
364
- }
365
- if (typeof node === "string" || typeof node === "number") {
366
- sink.append(escapeHtml(node));
367
- return;
368
- }
369
- if (Array.isArray(node)) {
370
- return appendReactNodeList(sink, node, state);
371
- }
372
- if (!isValidElement(node)) {
373
- return;
374
- }
375
- return appendReactElement(sink, node, state);
376
- }
377
- function appendReactNodeList(sink, nodes, state) {
378
- let chain;
379
- for (const node of nodes) {
380
- if (chain !== undefined) {
381
- chain = chain.then(() => appendReactNode(sink, node, state));
382
- continue;
383
- }
384
- const result = appendReactNode(sink, node, state);
385
- if (isPromiseLike(result)) {
386
- chain = result;
387
- }
388
- }
389
- return chain;
390
- }
391
- function appendReactElement(sink, element, state) {
392
- if (typeof element.type === "string") {
393
- return appendHostElement(sink, element, state);
394
- }
395
- if (element.type === Fragment) {
396
- return appendReactNode(sink, element.props.children, state);
397
- }
398
- if (element.type === ReactCompatSuspense) {
399
- return appendSuspenseElement(sink, element, state);
400
- }
401
- if (isClassComponentType(element.type)) {
402
- const instance = new element.type(element.props);
403
- return appendReactNode(sink, instance.render(), state);
404
- }
405
- if (typeof element.type === "function") {
406
- return appendReactNode(sink, element.type(element.props), state);
407
- }
408
- }
409
- function isClassComponentType(value) {
410
- return typeof value === "function" && value.prototype?.render !== undefined;
411
- }
412
- function appendHostElement(sink, element, state) {
413
- const tagName = element.type;
414
- const innerHtml = element.props
415
- .dangerouslySetInnerHTML;
416
- sink.append(`<${tagName}${renderHtmlAttributes(element.props)}>`);
417
- if (innerHtml !== undefined) {
418
- sink.append(String(innerHtml.__html ?? ""));
419
- sink.append(`</${tagName}>`);
420
- return;
421
- }
422
- const result = appendReactNode(sink, element.props.children, state);
423
- if (isPromiseLike(result)) {
424
- return result.then(() => {
425
- sink.append(`</${tagName}>`);
426
- });
427
- }
428
- sink.append(`</${tagName}>`);
429
- }
430
- function appendSuspenseElement(sink, element, state) {
431
- const rendered = renderReactNodeToString(element.props.children, state);
432
- if (!isPromiseLikeString(rendered)) {
433
- renderReactSuspenseBoundary(sink, (boundarySink) => {
434
- boundarySink.append(rendered);
435
- });
436
- return;
437
- }
438
- const id = state.suspenseId;
439
- state.suspenseId += 1;
440
- renderReactSuspenseOutOfOrderBoundary(sink, `B:${id}`, `S:${id}`, rendered, (boundarySink, renderedHtml) => {
441
- boundarySink.append(renderedHtml);
442
- }, {
443
- fallback(boundarySink) {
444
- const fallback = renderReactNodeToString(element.props.fallback, state);
445
- if (isPromiseLikeString(fallback)) {
446
- return fallback.then((html) => {
447
- boundarySink.append(html);
448
- });
449
- }
450
- boundarySink.append(fallback);
451
- },
452
- });
453
- }
454
- function renderReactNodeToString(node, state) {
455
- const sink = createStringSink();
456
- const result = appendReactNode(sink, node, state);
457
- if (isPromiseLike(result)) {
458
- return result.then(() => sink.toString());
459
- }
460
- return sink.toString();
461
- }
462
- function renderHtmlAttributes(props) {
463
- // Issue 078: <meta http-equiv="refresh" content="0;url=javascript:...">
464
- // is URL-bearing only when http-equiv is "refresh", so we need both
465
- // attributes in scope to make the call. Strip the unsafe content
466
- // before per-attribute rendering.
467
- const sanitizedProps = sanitizeMetaRefreshProps(props);
468
- return Object.entries(sanitizedProps)
469
- .map(([name, value]) => renderHtmlAttribute(name, value))
470
- .filter((attribute) => attribute !== "")
471
- .join("");
472
- }
473
- function sanitizeMetaRefreshProps(props) {
474
- const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
475
- const content = props["content"];
476
- if (typeof httpEquiv !== "string" || typeof content !== "string")
477
- return props;
478
- if (!isUnsafeMetaRefreshContent(httpEquiv, content))
479
- return props;
480
- // Drop only `content`; keep http-equiv so the developer's intent is
481
- // still visible in the rendered HTML (debugging hint).
482
- const sanitized = { ...props };
483
- delete sanitized["content"];
484
- return sanitized;
485
- }
486
- // Mirrors `isAttributeNameSafe` in react-dom: an attribute name must start with
487
- // an ASCII letter (or underscore) and contain only word chars, dot, hyphen, or
488
- // colon. Anything else is dropped to prevent SSR XSS via spread props
489
- // (`<div {...userControlled} />`). See docs/issues/resolved Issue 060.
490
- const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
491
- // URL scheme allow/block list is shared with the compiler emit paths
492
- // (Issues 062 / 073). See packages/server/src/url-safety.ts.
493
- function renderHtmlAttribute(name, value) {
494
- if (name === "children" ||
495
- name === "dangerouslySetInnerHTML" ||
496
- name === "key" ||
497
- name === "ref" ||
498
- value === false ||
499
- value === null ||
500
- value === undefined ||
501
- typeof value === "function") {
502
- return "";
503
- }
504
- const attributeName = toHtmlAttributeName(name);
505
- if (!VALID_ATTRIBUTE_NAME.test(attributeName)) {
506
- return "";
507
- }
508
- // Issue 077: HTML-bearing attributes (`srcdoc`) require the explicit
509
- // `{ __html: "..." }` opt-in. A plain string value -- even if escaped
510
- // for the attribute syntax -- decodes back to executable HTML inside
511
- // the iframe document with the parent's origin.
512
- if (isDangerousHtmlAttribute(attributeName)) {
513
- if (!isDangerousHtmlOptIn(value)) {
514
- return "";
515
- }
516
- return ` ${attributeName}="${escapeAttribute(value.__html)}"`;
517
- }
518
- if (value === true) {
519
- return ` ${attributeName}`;
520
- }
521
- const stringValue = String(value);
522
- if (isUnsafeUrlAttribute(attributeName, stringValue)) {
523
- return "";
524
- }
525
- return ` ${attributeName}="${escapeAttribute(stringValue)}"`;
526
- }
527
- function toHtmlAttributeName(name) {
528
- return HTML_ATTRIBUTE_ALIASES[name] ?? name;
529
- }
530
- const HTML_ATTRIBUTE_ALIASES = {
531
- acceptCharset: "accept-charset",
532
- autoFocus: "autofocus",
533
- autoPlay: "autoplay",
534
- charSet: "charset",
535
- className: "class",
536
- colSpan: "colspan",
537
- contentEditable: "contenteditable",
538
- crossOrigin: "crossorigin",
539
- encType: "enctype",
540
- formAction: "formaction",
541
- frameBorder: "frameborder",
542
- htmlFor: "for",
543
- httpEquiv: "http-equiv",
544
- maxLength: "maxlength",
545
- minLength: "minlength",
546
- noValidate: "novalidate",
547
- playsInline: "playsinline",
548
- readOnly: "readonly",
549
- rowSpan: "rowspan",
550
- spellCheck: "spellcheck",
551
- srcDoc: "srcdoc",
552
- srcSet: "srcset",
553
- tabIndex: "tabindex",
554
- useMap: "usemap",
555
- };
556
- function isPromiseLikeNode(value) {
557
- return isPromiseLikeUnknown(value);
558
- }
559
- function isPromiseLikeString(value) {
560
- return isPromiseLikeUnknown(value);
561
- }
562
- function isPromiseLike(value) {
563
- return isPromiseLikeUnknown(value);
564
- }
565
- function isPromiseLikeUnknown(value) {
566
- return (typeof value === "object" &&
567
- value !== null &&
568
- "then" in value &&
569
- typeof value.then === "function");
570
- }
571
- function renderNonceAttribute(nonce) {
572
- return nonce === undefined ? "" : ` nonce="${escapeAttribute(nonce)}"`;
573
- }
574
- export function renderToReadableStream(render, options = {}) {
575
- // Issue 084: append calls go into a coalescing Node Buffer sink. The
576
- // previous implementation called `controller.enqueue(encoder.encode(chunk))`
577
- // per `sink.append` — one TextEncoder allocation + one WHATWG queue trip
578
- // per call. Now we emit one chunk per flush boundary:
579
- // 1. After the sync portion of `render` returns — the "shell"
580
- // pre-flush. Done synchronously so it lands before any deferred
581
- // task body fires in a microtask.
582
- // 2. Whenever the accumulated buffer crosses the flushThreshold
583
- // mid-render (e.g. a single very large list rendering).
584
- // 3. Each `sink.append` made during the deferred phase flushes
585
- // immediately — gives each OOB fragment its own HTTP chunk so
586
- // the browser can swap it in as soon as it arrives.
587
- // 4. End of stream — any tail bytes.
588
- const abortController = new AbortController();
589
- const queuedChunks = [];
590
- let controllerRef;
591
- let cancelled = false;
592
- let complete = false;
593
- let queuedBytes = 0;
594
- let warnedQueuedBytes = false;
595
- let backpressurePromise;
596
- let resolveBackpressure;
597
- const enqueueOrQueue = (buffer) => {
598
- if (cancelled || abortController.signal.aborted) {
599
- return;
600
- }
601
- const controller = controllerRef;
602
- if (controller === undefined) {
603
- queueChunk(buffer);
604
- return;
605
- }
606
- if (queuedChunks.length === 0 && (controller.desiredSize ?? 0) > 0) {
607
- controller.enqueue(buffer);
608
- resolveBackpressureIfReady();
609
- return;
610
- }
611
- queueChunk(buffer);
612
- };
613
- const drainQueuedChunks = (controller) => {
614
- while (!cancelled && queuedChunks.length > 0 && (controller.desiredSize ?? 0) > 0) {
615
- const chunk = queuedChunks.shift();
616
- if (chunk !== undefined) {
617
- queuedBytes -= chunk.byteLength;
618
- controller.enqueue(chunk);
619
- }
620
- }
621
- if (!cancelled && complete && queuedChunks.length === 0) {
622
- controller.close();
623
- }
624
- resolveBackpressureIfReady();
625
- };
626
- return new ReadableStream({
627
- start(controller) {
628
- controllerRef = controller;
629
- const sink = createStreamingBufferSink({
630
- onFlush(buffer) {
631
- enqueueOrQueue(buffer);
632
- },
633
- });
634
- const deferredTasks = [];
635
- let inDeferredPhase = false;
636
- let renderResult;
637
- try {
638
- renderResult = render({
639
- append(chunk) {
640
- if (abortController.signal.aborted) {
641
- return;
642
- }
643
- sink.append(chunk);
644
- if (inDeferredPhase) {
645
- // OOB pattern: each deferred task ends with exactly one
646
- // `sink.append("<template ...>...")`. Flushing here
647
- // promotes that single append to its own chunk so the
648
- // browser's MutationObserver can apply it without
649
- // waiting for other deferred fragments.
650
- sink.flush();
651
- }
652
- },
653
- backpressure() {
654
- return waitForBackpressure();
655
- },
656
- defer(task) {
657
- deferredTasks.push(ignoreAfterAbort(task, abortController.signal, options));
658
- },
659
- signal: abortController.signal,
660
- });
661
- }
662
- catch (error) {
663
- if (abortController.signal.aborted) {
664
- return;
665
- }
666
- controller.error(error);
667
- return;
668
- }
669
- // Shell pre-flush — synchronous, BEFORE we yield to microtasks.
670
- // If we awaited render first the deferred tasks' bodies would
671
- // already have appended their bytes to the same buffer and we
672
- // would emit one merged chunk.
673
- sink.flush();
674
- void continueAfterShell();
675
- async function continueAfterShell() {
676
- try {
677
- if (renderResult !== undefined && renderResult !== null) {
678
- await raceAbort(renderResult, abortController.signal);
679
- // Async render may have written more before its tail returned.
680
- // That tail is also "shell" — flush it before entering the
681
- // deferred phase.
682
- sink.flush();
683
- }
684
- inDeferredPhase = true;
685
- await raceAbort(Promise.all(deferredTasks), abortController.signal);
686
- // Tail flush in case the render closure (or a deferred task)
687
- // somehow left bytes in the buffer past the per-append flushes.
688
- sink.flush();
689
- complete = true;
690
- drainQueuedChunks(controller);
691
- }
692
- catch (error) {
693
- if (abortController.signal.aborted) {
694
- return;
695
- }
696
- controller.error(error);
697
- }
698
- }
699
- },
700
- pull(controller) {
701
- drainQueuedChunks(controller);
702
- resolveBackpressureAfterPull();
703
- },
704
- cancel(reason) {
705
- cancelled = true;
706
- queuedChunks.length = 0;
707
- queuedBytes = 0;
708
- abortController.abort(reason);
709
- resolveBackpressureIfReady();
710
- },
711
- });
712
- function hasBackpressure() {
713
- if (cancelled || abortController.signal.aborted) {
714
- return false;
715
- }
716
- if (queuedChunks.length > 0) {
717
- return true;
718
- }
719
- const controller = controllerRef;
720
- return controller !== undefined && (controller.desiredSize ?? 0) <= 0;
721
- }
722
- function waitForBackpressure() {
723
- if (!hasBackpressure()) {
724
- return Promise.resolve();
725
- }
726
- if (backpressurePromise === undefined) {
727
- backpressurePromise = new Promise((resolve) => {
728
- resolveBackpressure = resolve;
729
- });
730
- }
731
- return backpressurePromise;
732
- }
733
- function resolveBackpressureIfReady() {
734
- if (resolveBackpressure === undefined || hasBackpressure()) {
735
- return;
736
- }
737
- resolveBackpressureWaiter();
738
- }
739
- function resolveBackpressureAfterPull() {
740
- if (resolveBackpressure === undefined ||
741
- queuedChunks.length > 0 ||
742
- cancelled ||
743
- abortController.signal.aborted) {
744
- return;
745
- }
746
- resolveBackpressureWaiter();
747
- }
748
- function resolveBackpressureWaiter() {
749
- const resolve = resolveBackpressure;
750
- if (resolve === undefined) {
751
- return;
752
- }
753
- backpressurePromise = undefined;
754
- resolveBackpressure = undefined;
755
- resolve();
756
- }
757
- function queueChunk(buffer) {
758
- queuedChunks.push(buffer);
759
- queuedBytes += buffer.byteLength;
760
- if (!warnedQueuedBytes &&
761
- queuedBytes > streamQueuedChunkSoftLimitBytes &&
762
- shouldWarnAboutQueuedStreamBytes()) {
763
- warnedQueuedBytes = true;
764
- console.warn(`[mreact] renderToReadableStream queued ${queuedBytes} bytes because the downstream reader is slower than the renderer.`);
765
- }
766
- }
767
- }
768
- function shouldWarnAboutQueuedStreamBytes() {
769
- return (typeof process !== "undefined" &&
770
- process.env !== undefined &&
771
- process.env.NODE_ENV !== "production");
772
- }
773
- async function raceAbort(task, signal) {
774
- if (signal.aborted) {
775
- return undefined;
776
- }
777
- return Promise.race([
778
- task,
779
- new Promise((resolve) => {
780
- signal.addEventListener("abort", () => resolve(undefined), { once: true });
781
- }),
782
- ]);
783
- }
784
- function ignoreAfterAbort(task, signal, options) {
785
- return Promise.resolve(task).catch((error) => {
786
- if (!signal.aborted) {
787
- throw error;
788
- }
789
- if (options.logAbortedDeferredErrors === true && process.env.NODE_ENV !== "production") {
790
- console.warn("[mreact] ignored deferred task error after abort:", error);
791
- }
792
- });
793
- }
794
- function escapeAttribute(value) {
795
- return value
796
- .replaceAll("&", "&amp;")
797
- .replaceAll('"', "&quot;")
798
- .replaceAll("<", "&lt;")
799
- .replaceAll(">", "&gt;");
800
- }
801
- const outOfOrderReorderScript = `(()=>{function apply(root){const fragments=Array.from(root.querySelectorAll("template[data-mreact-oob-fragment]"));for(const fragment of fragments){const id=fragment.getAttribute("data-mreact-oob-fragment");if(id===null)continue;const placeholders=Array.from(root.querySelectorAll("[data-mreact-oob-placeholder]"));const placeholder=placeholders.find((candidate)=>candidate.getAttribute("data-mreact-oob-placeholder")===id);if(placeholder===undefined)continue;placeholder.replaceWith(fragment.content.cloneNode(true));fragment.remove();}}apply(document);new MutationObserver(()=>apply(document)).observe(document.documentElement,{childList:true,subtree:true});})();`;
802
- const reactSuspenseRevealScriptBody = `(self.$RC=self.$RC||function(bid,sid){var b=document.getElementById(bid);var s=document.getElementById(sid);if(!b||!s)return;var p=b.parentNode;var e=b.nextSibling;var d=0;var r=[];for(var n=e;n;n=n.nextSibling){if(n.nodeType===8){if(n.data==="$"||n.data==="$?"||n.data==="$!")d++;else if(n.data==="/$"){if(d===0){e=n;break;}d--;}}r.push(n);}for(var i=0;r[i];i++)p.removeChild(r[i]);while(s.firstChild)p.insertBefore(s.firstChild,e);s.remove();b.data="$";})`;
803
- export const reactSuspenseRevealExternalScript = `(()=>{${reactSuspenseRevealScriptBody};var s=document.currentScript;if(!s)return;var b=s.getAttribute("data-boundary-id");var seg=s.getAttribute("data-segment-id");if(b!==null&&seg!==null)self.$RC(b,seg);})();`;
804
15
  //# sourceMappingURL=index.js.map