@reckona/mreact-compiler 0.0.65 → 0.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1827 @@
1
+ import type {
2
+ AttributeIr,
3
+ ComponentPropIr,
4
+ ComponentIr,
5
+ JsxNodeIr,
6
+ ModuleIr,
7
+ } from "./ir.js";
8
+ import type { RuntimeImport, ServerEscapeOptions } from "./types.js";
9
+ import { emitEscapeHtmlHelper } from "./emit-escape-helper.js";
10
+ import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
11
+ import {
12
+ htmlAttributeName,
13
+ isDangerousHtmlAttribute,
14
+ isStaticUrlValueUnsafe,
15
+ isUrlAttribute,
16
+ parseStaticStyleObjectLiteral,
17
+ parseStyleLiteralValue,
18
+ simpleSideEffectFreeExpression,
19
+ } from "./emit-server-shared.js";
20
+ import { oxcServerStringReactNodeRenderHelperPlaceholder } from "./oxc-runtime-emit.js";
21
+
22
+ export interface EmitResult {
23
+ code: string;
24
+ imports: RuntimeImport[];
25
+ }
26
+
27
+ export interface EmitServerOptions {
28
+ dynamicAttributes?: "drop" | "emit";
29
+ escape?: ServerEscapeOptions | undefined;
30
+ serverHydration?: boolean;
31
+ }
32
+
33
+ // Module-local handle to the URL-safety helper name for the current emit
34
+ // call. Used by deeply-nested attribute emitters to avoid threading the
35
+ // name through every signature. Reset at the top of `emitServer`.
36
+ let currentUrlSafeHelperName: string = "_urlAttrSafe";
37
+ let currentClientBoundaryHelperName: string | undefined;
38
+ let currentSpreadAttributesHelperName: string = "_renderSpreadAttributes";
39
+
40
+ export function emitServer(
41
+ ir: ModuleIr,
42
+ options: EmitServerOptions = {},
43
+ ): EmitResult {
44
+ const escapeHelperName = allocateEscapeHelperName(ir);
45
+ const escapeBatchHelperName = options.escape === undefined
46
+ ? undefined
47
+ : allocateHelperName(ir, "_escapeHtmlBatch");
48
+ const contextProviderHelperName = usesContextProvider(ir)
49
+ ? allocateHelperName(ir, "_renderContextProviderToString")
50
+ : undefined;
51
+ const contextConsumerHelperName = usesContextConsumer(ir)
52
+ ? allocateHelperName(ir, "_renderContextConsumerToString")
53
+ : undefined;
54
+ const reactNodeRenderHelperName = usesReactNodeRender(ir)
55
+ ? allocateHelperName(ir, "_renderReactNodeToString")
56
+ : undefined;
57
+ const clientBoundaryHelperName = usesClientBoundary(ir)
58
+ ? allocateHelperName(ir, "_renderClientBoundary")
59
+ : undefined;
60
+ const spreadAttributesHelperName = allocateHelperName(ir, "_renderSpreadAttributes");
61
+ const outAccumulatorName = allocateHelperName(ir, "_out");
62
+ const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe");
63
+ currentUrlSafeHelperName = urlSafeHelperName;
64
+ currentClientBoundaryHelperName = clientBoundaryHelperName;
65
+ currentSpreadAttributesHelperName = spreadAttributesHelperName;
66
+ const helper = emitEscapeHtmlHelper(escapeHelperName);
67
+ // Inline URL-scheme guard mirroring packages/server/src/url-safety.ts.
68
+ // Returns the original value when safe to emit and undefined when the
69
+ // attribute should be dropped. Inlined so compiler output stays free
70
+ // of cross-package runtime imports.
71
+ // Mirrors packages/server/src/url-safety.ts. Issue 078: in-scheme
72
+ // tab/CR/LF must be stripped anywhere in the value, not just at the
73
+ // start, to match the browser's URL parser.
74
+ const urlSafeHelper = [
75
+ `function ${urlSafeHelperName}(name, value) {`,
76
+ ` if (typeof value !== "string") return value;`,
77
+ ` const _canonical = value`,
78
+ ` .replace(/^[\\x00-\\x20]+/u, "")`,
79
+ ` .replace(/[\\t\\r\\n]/g, "");`,
80
+ ` const _match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(_canonical);`,
81
+ ` if (_match === null) return value;`,
82
+ ` const _scheme = _match[1].toLowerCase();`,
83
+ ` if (_scheme !== "javascript" && _scheme !== "vbscript" && _scheme !== "livescript" && _scheme !== "mhtml" && _scheme !== "file" && _scheme !== "data") return value;`,
84
+ ` if (_scheme === "data" && (name === "src" || name === "poster") && /^data:image\\/(?!svg\\+xml(?:[;,]|$))/i.test(_canonical)) return value;`,
85
+ ` return undefined;`,
86
+ `}`,
87
+ ].join("\n");
88
+ const asyncComponentNames = collectAsyncServerComponentNames(ir.components);
89
+ const components = ir.components
90
+ .map((component) =>
91
+ emitComponent(
92
+ component,
93
+ escapeHelperName,
94
+ escapeBatchHelperName,
95
+ outAccumulatorName,
96
+ options,
97
+ asyncComponentNames,
98
+ options.dynamicAttributes ?? "emit",
99
+ contextProviderHelperName,
100
+ contextConsumerHelperName,
101
+ reactNodeRenderHelperName,
102
+ ),
103
+ )
104
+ .join("\n\n");
105
+ // Tree-shake the URL-safety helper when it is not referenced by any
106
+ // component output. Same shape as the existing escapeImport check.
107
+ const needsSpreadAttributesHelper = components.includes(spreadAttributesHelperName);
108
+ const urlSafeBlock =
109
+ components.includes(urlSafeHelperName) || needsSpreadAttributesHelper ? urlSafeHelper : "";
110
+ const clientBoundaryBlock =
111
+ clientBoundaryHelperName === undefined || !components.includes(clientBoundaryHelperName)
112
+ ? ""
113
+ : emitClientBoundaryHelper(clientBoundaryHelperName);
114
+ const spreadAttributesBlock = needsSpreadAttributesHelper
115
+ ? emitSpreadAttributesHelper(spreadAttributesHelperName, escapeHelperName, urlSafeHelperName)
116
+ : "";
117
+ // Emit batch escape import only when the helper is actually referenced
118
+ // by the generated component code (issue 048: dead-import elimination).
119
+ // Helper names are uniquely allocated, so a literal substring check is
120
+ // both correct and inexpensive.
121
+ const escapeImport =
122
+ options.escape === undefined ||
123
+ escapeBatchHelperName === undefined ||
124
+ !components.includes(escapeBatchHelperName)
125
+ ? ""
126
+ : `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`;
127
+ const userImports = emitUserImports(ir);
128
+ const contextImport = emitContextImport(
129
+ contextProviderHelperName,
130
+ contextConsumerHelperName,
131
+ reactNodeRenderHelperName,
132
+ );
133
+ const moduleStatements = emitModuleStatements(ir);
134
+
135
+ return {
136
+ code: `${[userImports, escapeImport, contextImport, moduleStatements, helper, urlSafeBlock, clientBoundaryBlock, spreadAttributesBlock].filter(Boolean).join("\n\n")}\n\n${components}\n`,
137
+ imports: collectContextImports(
138
+ contextProviderHelperName,
139
+ contextConsumerHelperName,
140
+ reactNodeRenderHelperName,
141
+ ),
142
+ };
143
+ }
144
+
145
+ function emitContextImport(
146
+ contextProviderHelperName: string | undefined,
147
+ contextConsumerHelperName: string | undefined,
148
+ reactNodeRenderHelperName: string | undefined,
149
+ ): string {
150
+ const specifiers = [
151
+ reactNodeRenderHelperName === undefined
152
+ ? undefined
153
+ : `renderToString as ${reactNodeRenderHelperName}`,
154
+ contextProviderHelperName === undefined
155
+ ? undefined
156
+ : `renderContextProviderToString as ${contextProviderHelperName}`,
157
+ contextConsumerHelperName === undefined
158
+ ? undefined
159
+ : `renderContextConsumerToString as ${contextConsumerHelperName}`,
160
+ ].filter((specifier): specifier is string => specifier !== undefined);
161
+
162
+ return specifiers.length === 0
163
+ ? ""
164
+ : `import { ${specifiers.join(", ")} } from "@reckona/mreact-compat";`;
165
+ }
166
+
167
+ function collectContextImports(
168
+ contextProviderHelperName: string | undefined,
169
+ contextConsumerHelperName: string | undefined,
170
+ reactNodeRenderHelperName?: string,
171
+ ): RuntimeImport[] {
172
+ const specifiers = [
173
+ reactNodeRenderHelperName === undefined ? undefined : "renderToString",
174
+ contextProviderHelperName === undefined
175
+ ? undefined
176
+ : "renderContextProviderToString",
177
+ contextConsumerHelperName === undefined
178
+ ? undefined
179
+ : "renderContextConsumerToString",
180
+ ].filter((specifier): specifier is string => specifier !== undefined);
181
+
182
+ return specifiers.length === 0
183
+ ? []
184
+ : [{ source: "@reckona/mreact-compat", specifiers }];
185
+ }
186
+
187
+ function emitUserImports(ir: ModuleIr): string {
188
+ return ir.userImports.join("\n");
189
+ }
190
+
191
+ function emitModuleStatements(ir: ModuleIr): string {
192
+ return ir.moduleStatements.join("\n");
193
+ }
194
+
195
+ function emitComponent(
196
+ component: ComponentIr,
197
+ escapeHelperName: string,
198
+ escapeBatchHelperName: string | undefined,
199
+ outAccumulatorName: string,
200
+ options: EmitServerOptions,
201
+ asyncComponentNames: ReadonlySet<string>,
202
+ dynamicAttributes: "drop" | "emit",
203
+ contextProviderHelperName?: string,
204
+ contextConsumerHelperName?: string,
205
+ reactNodeRenderHelperName?: string,
206
+ ): string {
207
+ const body = component.bodyStatements.map((statement) =>
208
+ ` ${replaceOxcServerStringReactNodeRenderHelper(statement, reactNodeRenderHelperName)}`,
209
+ );
210
+ const parameters = component.parameters.join(", ");
211
+ const htmlStatements = collectHtmlStatements(
212
+ component.root,
213
+ outAccumulatorName,
214
+ escapeHelperName,
215
+ escapeBatchHelperName,
216
+ asyncComponentNames,
217
+ dynamicAttributes,
218
+ contextProviderHelperName,
219
+ contextConsumerHelperName,
220
+ reactNodeRenderHelperName,
221
+ );
222
+
223
+ const markerStart = stringLiteral(`<!--mreact-h:start:${encodeURIComponent(component.name)}-->`);
224
+ const markerEnd = stringLiteral(`<!--mreact-h:end:${encodeURIComponent(component.name)}-->`);
225
+
226
+ const hydrationOpenStatements =
227
+ options.serverHydration === true ? [` ${outAccumulatorName} += ${markerStart};`] : [];
228
+ const hydrationCloseStatements =
229
+ options.serverHydration === true ? [` ${outAccumulatorName} += ${markerEnd};`] : [];
230
+
231
+ const functionKeyword = `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}${
232
+ asyncComponentNames.has(component.name) ? "async " : ""
233
+ }function`;
234
+
235
+ return [
236
+ `${functionKeyword} ${component.name}(${parameters}) {`,
237
+ ...body,
238
+ ` let ${outAccumulatorName} = "";`,
239
+ ...hydrationOpenStatements,
240
+ ...htmlStatements.map((statement) => ` ${statement}`),
241
+ ...hydrationCloseStatements,
242
+ ` return ${outAccumulatorName};`,
243
+ `}`,
244
+ ].join("\n");
245
+ }
246
+
247
+ function emitHtmlExpression(
248
+ node: JsxNodeIr,
249
+ escapeHelperName: string,
250
+ escapeBatchHelperName: string | undefined,
251
+ asyncComponentNames: ReadonlySet<string>,
252
+ dynamicAttributes: "drop" | "emit",
253
+ contextProviderHelperName?: string,
254
+ contextConsumerHelperName?: string,
255
+ reactNodeRenderHelperName?: string,
256
+ ): string {
257
+ const parts = collectHtmlParts(
258
+ node,
259
+ escapeHelperName,
260
+ escapeBatchHelperName,
261
+ asyncComponentNames,
262
+ dynamicAttributes,
263
+ contextProviderHelperName,
264
+ contextConsumerHelperName,
265
+ reactNodeRenderHelperName,
266
+ );
267
+
268
+ if (parts.length === 0) {
269
+ return "\"\"";
270
+ }
271
+
272
+ return parts.join(" + ");
273
+ }
274
+
275
+ /**
276
+ * Statement-list IR walker (issue 046 followup). Produces a sequence of
277
+ * statements that each append to a shared accumulator variable instead of
278
+ * a single concat expression. Used at the top of component bodies; sub
279
+ * callbacks (`renderContextProviderToString`, async list renderers, etc.)
280
+ * still use the expression form via `emitHtmlExpression`.
281
+ *
282
+ * The benefits over expression mode:
283
+ * - intermediate string allocations from `+ +` chains disappear
284
+ * - conditional branches lower to `if/else` (no ternary expression spaghetti)
285
+ * - sync list rendering inlines the for-loop append without an IIFE wrapper
286
+ * - debugger / source maps step naturally over the generated statements
287
+ */
288
+ function collectHtmlStatements(
289
+ node: JsxNodeIr,
290
+ outVar: string,
291
+ escapeHelperName: string,
292
+ escapeBatchHelperName: string | undefined,
293
+ asyncComponentNames: ReadonlySet<string>,
294
+ dynamicAttributes: "drop" | "emit",
295
+ contextProviderHelperName?: string,
296
+ contextConsumerHelperName?: string,
297
+ reactNodeRenderHelperName?: string,
298
+ selectedValueCode?: string,
299
+ ): string[] {
300
+ if (node.kind === "text") {
301
+ const literal = escapeHtml(node.value);
302
+ if (literal === "") {
303
+ return [];
304
+ }
305
+ return [`${outVar} += ${stringLiteral(literal)};`];
306
+ }
307
+
308
+ if (node.kind === "expr") {
309
+ if (node.renderMode === "html") {
310
+ return [`${outVar} += ${rawHtmlExpression(node.code)};`];
311
+ }
312
+
313
+ if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
314
+ return [`${outVar} += ${reactNodeRenderHelperName}(() => (${node.code}));`];
315
+ }
316
+
317
+ return [`${outVar} += ${escapeHelperName}(${node.code});`];
318
+ }
319
+
320
+ if (node.kind === "conditional") {
321
+ const whenTrueStatements = node.whenTrue.flatMap((child) =>
322
+ collectHtmlStatements(
323
+ child,
324
+ outVar,
325
+ escapeHelperName,
326
+ escapeBatchHelperName,
327
+ asyncComponentNames,
328
+ dynamicAttributes,
329
+ contextProviderHelperName,
330
+ contextConsumerHelperName,
331
+ reactNodeRenderHelperName,
332
+ ),
333
+ );
334
+ const whenFalseStatements = node.whenFalse.flatMap((child) =>
335
+ collectHtmlStatements(
336
+ child,
337
+ outVar,
338
+ escapeHelperName,
339
+ escapeBatchHelperName,
340
+ asyncComponentNames,
341
+ dynamicAttributes,
342
+ contextProviderHelperName,
343
+ contextConsumerHelperName,
344
+ reactNodeRenderHelperName,
345
+ ),
346
+ );
347
+
348
+ if (whenTrueStatements.length === 0 && whenFalseStatements.length === 0) {
349
+ return [];
350
+ }
351
+
352
+ if (whenFalseStatements.length === 0) {
353
+ return [
354
+ `if (${node.conditionCode}) {`,
355
+ ...whenTrueStatements.map((statement) => ` ${statement}`),
356
+ `}`,
357
+ ];
358
+ }
359
+
360
+ if (whenTrueStatements.length === 0) {
361
+ return [
362
+ `if (!(${node.conditionCode})) {`,
363
+ ...whenFalseStatements.map((statement) => ` ${statement}`),
364
+ `}`,
365
+ ];
366
+ }
367
+
368
+ return [
369
+ `if (${node.conditionCode}) {`,
370
+ ...whenTrueStatements.map((statement) => ` ${statement}`),
371
+ `} else {`,
372
+ ...whenFalseStatements.map((statement) => ` ${statement}`),
373
+ `}`,
374
+ ];
375
+ }
376
+
377
+ if (node.kind === "list") {
378
+ const isAsync = containsAsyncServerOperationInChildren(
379
+ node.children,
380
+ asyncComponentNames,
381
+ );
382
+
383
+ if (isAsync) {
384
+ // Parallel async path keeps the existing renderer + Promise.all + join
385
+ // form to preserve concurrent resolution semantics.
386
+ const parameters =
387
+ node.indexName === undefined
388
+ ? node.itemName
389
+ : `${node.itemName}, ${node.indexName}`;
390
+ const renderer = emitListRenderer(
391
+ node,
392
+ parameters,
393
+ escapeHelperName,
394
+ escapeBatchHelperName,
395
+ asyncComponentNames,
396
+ dynamicAttributes,
397
+ contextProviderHelperName,
398
+ contextConsumerHelperName,
399
+ reactNodeRenderHelperName,
400
+ );
401
+ const mapped = `(${node.itemsCode}).map(${renderer})`;
402
+ return [`${outVar} += (await Promise.all(${mapped})).join("");`];
403
+ }
404
+
405
+ // Sync list — inline for-loop appending to the caller's accumulator.
406
+ // No inner IIFE wrapper and no intermediate string concat per iteration.
407
+ const itemBinding = `const ${node.itemName} = _arr[_i];`;
408
+ const indexBinding =
409
+ node.indexName === undefined ? undefined : `const ${node.indexName} = _i;`;
410
+ const bodyStatements = node.bodyStatements ?? [];
411
+ const childStatements = node.children.flatMap((child) =>
412
+ collectHtmlStatements(
413
+ child,
414
+ outVar,
415
+ escapeHelperName,
416
+ escapeBatchHelperName,
417
+ asyncComponentNames,
418
+ dynamicAttributes,
419
+ contextProviderHelperName,
420
+ contextConsumerHelperName,
421
+ reactNodeRenderHelperName,
422
+ ),
423
+ );
424
+
425
+ return [
426
+ `{`,
427
+ ` const _arr = (${node.itemsCode});`,
428
+ ` for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`,
429
+ ` ${itemBinding}`,
430
+ ...(indexBinding === undefined ? [] : [` ${indexBinding}`]),
431
+ ...bodyStatements.map((statement) => ` ${statement}`),
432
+ ...childStatements.map((statement) => ` ${statement}`),
433
+ ` }`,
434
+ `}`,
435
+ ];
436
+ }
437
+
438
+ if (node.kind === "fragment") {
439
+ return node.children.flatMap((child) =>
440
+ collectHtmlStatements(
441
+ child,
442
+ outVar,
443
+ escapeHelperName,
444
+ escapeBatchHelperName,
445
+ asyncComponentNames,
446
+ dynamicAttributes,
447
+ contextProviderHelperName,
448
+ contextConsumerHelperName,
449
+ reactNodeRenderHelperName,
450
+ ),
451
+ );
452
+ }
453
+
454
+ if (node.kind === "component") {
455
+ if (node.name === "Suspense") {
456
+ return [
457
+ `${outVar} += "<!--$-->";`,
458
+ ...node.children.flatMap((child) =>
459
+ collectHtmlStatements(
460
+ child,
461
+ outVar,
462
+ escapeHelperName,
463
+ escapeBatchHelperName,
464
+ asyncComponentNames,
465
+ dynamicAttributes,
466
+ contextProviderHelperName,
467
+ contextConsumerHelperName,
468
+ reactNodeRenderHelperName,
469
+ ),
470
+ ),
471
+ `${outVar} += "<!--/$-->";`,
472
+ ];
473
+ }
474
+
475
+ if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
476
+ // Provider helper takes a string-returning callback. Use the
477
+ // expression form inside the callback to preserve the existing
478
+ // helper contract.
479
+ const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
480
+ return [
481
+ `${outVar} += ${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
482
+ ];
483
+ }
484
+
485
+ if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
486
+ const renderProp = findComponentRenderProp(node.props, "children");
487
+
488
+ if (renderProp !== undefined) {
489
+ const valueName = renderProp.valueName ?? "_value";
490
+ return [
491
+ `${outVar} += ${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
492
+ ];
493
+ }
494
+ }
495
+
496
+ if (isClientBoundaryPlaceholder(node)) {
497
+ const helperName = currentClientBoundaryHelperName;
498
+ if (helperName !== undefined) {
499
+ return [
500
+ `${outVar} += ${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(
501
+ node.props,
502
+ node.children,
503
+ escapeHelperName,
504
+ escapeBatchHelperName,
505
+ asyncComponentNames,
506
+ dynamicAttributes,
507
+ contextProviderHelperName,
508
+ contextConsumerHelperName,
509
+ reactNodeRenderHelperName,
510
+ )});`,
511
+ ];
512
+ }
513
+
514
+ return [`${outVar} += ${stringLiteral(clientBoundaryPlaceholder(node))};`];
515
+ }
516
+
517
+ if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
518
+ return [
519
+ `${outVar} += ${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(
520
+ node.props,
521
+ node.children,
522
+ escapeHelperName,
523
+ escapeBatchHelperName,
524
+ asyncComponentNames,
525
+ dynamicAttributes,
526
+ contextProviderHelperName,
527
+ contextConsumerHelperName,
528
+ reactNodeRenderHelperName,
529
+ )});`,
530
+ ];
531
+ }
532
+
533
+ return [
534
+ `${outVar} += ${emitComponentCallExpression(
535
+ node.name,
536
+ emitPropsObject(
537
+ node.props,
538
+ node.children,
539
+ escapeHelperName,
540
+ escapeBatchHelperName,
541
+ asyncComponentNames,
542
+ dynamicAttributes,
543
+ contextProviderHelperName,
544
+ contextConsumerHelperName,
545
+ reactNodeRenderHelperName,
546
+ ),
547
+ asyncComponentNames,
548
+ )};`,
549
+ ];
550
+ }
551
+
552
+ if (node.kind === "async-boundary") {
553
+ return [];
554
+ }
555
+
556
+ // element
557
+ const statements: string[] = [];
558
+ if (node.tagName === "textarea") {
559
+ const attributeScan = scanElementAttributes(node.tagName, node.attributes);
560
+ statements.push(`${outVar} += ${stringLiteral("<textarea")};`);
561
+ for (const attributePart of collectElementAttributeParts(
562
+ node.tagName,
563
+ node.attributes,
564
+ escapeHelperName,
565
+ escapeBatchHelperName,
566
+ dynamicAttributes,
567
+ attributeScan,
568
+ )) {
569
+ statements.push(`${outVar} += ${attributePart};`);
570
+ }
571
+ statements.push(`${outVar} += ">";`);
572
+ for (const valuePart of collectTextareaValueParts(
573
+ node,
574
+ escapeHelperName,
575
+ escapeBatchHelperName,
576
+ asyncComponentNames,
577
+ dynamicAttributes,
578
+ contextProviderHelperName,
579
+ contextConsumerHelperName,
580
+ reactNodeRenderHelperName,
581
+ attributeScan,
582
+ )) {
583
+ statements.push(`${outVar} += ${valuePart};`);
584
+ }
585
+ statements.push(`${outVar} += "</textarea>";`);
586
+ return statements;
587
+ }
588
+
589
+ statements.push(`${outVar} += ${stringLiteral(`<${node.tagName}`)};`);
590
+ const attributeScan = scanElementAttributes(node.tagName, node.attributes);
591
+
592
+ for (const attributePart of collectElementAttributeParts(
593
+ node.tagName,
594
+ node.attributes,
595
+ escapeHelperName,
596
+ escapeBatchHelperName,
597
+ dynamicAttributes,
598
+ attributeScan,
599
+ )) {
600
+ statements.push(`${outVar} += ${attributePart};`);
601
+ }
602
+ const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
603
+ if (selectedAttributePart !== undefined) {
604
+ statements.push(`${outVar} += ${selectedAttributePart};`);
605
+ }
606
+
607
+ statements.push(`${outVar} += ">";`);
608
+
609
+ const dangerousInnerHtml = emitDangerouslySetInnerHtmlExpression(node.attributes);
610
+ if (dangerousInnerHtml !== undefined) {
611
+ statements.push(`${outVar} += ${dangerousInnerHtml};`);
612
+ statements.push(`${outVar} += ${stringLiteral(`</${node.tagName}>`)};`);
613
+ return statements;
614
+ }
615
+
616
+ const childrenExpression = emitBatchedSimpleChildrenExpression(
617
+ node.children,
618
+ escapeBatchHelperName,
619
+ );
620
+ const childSelectedValueCode = node.tagName === "select"
621
+ ? attributeScan.formValueAttributeCode
622
+ : undefined;
623
+
624
+ if (childrenExpression !== undefined && childSelectedValueCode === undefined) {
625
+ statements.push(`${outVar} += ${childrenExpression};`);
626
+ } else {
627
+ for (const child of node.children) {
628
+ statements.push(
629
+ ...collectHtmlStatements(
630
+ child,
631
+ outVar,
632
+ escapeHelperName,
633
+ escapeBatchHelperName,
634
+ asyncComponentNames,
635
+ dynamicAttributes,
636
+ contextProviderHelperName,
637
+ contextConsumerHelperName,
638
+ reactNodeRenderHelperName,
639
+ childSelectedValueCode,
640
+ ),
641
+ );
642
+ }
643
+ }
644
+
645
+ statements.push(`${outVar} += ${stringLiteral(`</${node.tagName}>`)};`);
646
+
647
+ return statements;
648
+ }
649
+
650
+ function collectHtmlParts(
651
+ node: JsxNodeIr,
652
+ escapeHelperName: string,
653
+ escapeBatchHelperName: string | undefined,
654
+ asyncComponentNames: ReadonlySet<string>,
655
+ dynamicAttributes: "drop" | "emit",
656
+ contextProviderHelperName?: string,
657
+ contextConsumerHelperName?: string,
658
+ reactNodeRenderHelperName?: string,
659
+ selectedValueCode?: string,
660
+ ): string[] {
661
+ if (node.kind === "text") {
662
+ return [stringLiteral(escapeHtml(node.value))];
663
+ }
664
+
665
+ if (node.kind === "expr") {
666
+ if (node.renderMode === "html") {
667
+ return [rawHtmlExpression(node.code)];
668
+ }
669
+
670
+ if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
671
+ return [`${reactNodeRenderHelperName}(() => (${node.code}))`];
672
+ }
673
+
674
+ return [`${escapeHelperName}(${node.code})`];
675
+ }
676
+
677
+ if (node.kind === "conditional") {
678
+ return [
679
+ `((${node.conditionCode}) ? ${emitHtmlExpressionFromChildren(node.whenTrue, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)} : ${emitHtmlExpressionFromChildren(node.whenFalse, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
680
+ ];
681
+ }
682
+
683
+ if (node.kind === "list") {
684
+ const isAsync = containsAsyncServerOperationInChildren(
685
+ node.children,
686
+ asyncComponentNames,
687
+ );
688
+
689
+ if (isAsync) {
690
+ // Async lists rely on Promise.all() for parallel resolution; the
691
+ // callback allocation is amortized across `await` latency, so we keep
692
+ // the `.map().then(...).join("")` form.
693
+ const parameters =
694
+ node.indexName === undefined
695
+ ? node.itemName
696
+ : `${node.itemName}, ${node.indexName}`;
697
+ const renderer = emitListRenderer(
698
+ node,
699
+ parameters,
700
+ escapeHelperName,
701
+ escapeBatchHelperName,
702
+ asyncComponentNames,
703
+ dynamicAttributes,
704
+ contextProviderHelperName,
705
+ contextConsumerHelperName,
706
+ reactNodeRenderHelperName,
707
+ );
708
+ const mapped = `(${node.itemsCode}).map(${renderer})`;
709
+ return [`(await Promise.all(${mapped})).join("")`];
710
+ }
711
+
712
+ // Synchronous list — imperative accumulator avoids the per-render
713
+ // callback allocation, the intermediate `.map()` result array, and the
714
+ // trailing `.join("")` call.
715
+ return [emitSyncListIife(
716
+ node,
717
+ escapeHelperName,
718
+ escapeBatchHelperName,
719
+ asyncComponentNames,
720
+ dynamicAttributes,
721
+ contextProviderHelperName,
722
+ contextConsumerHelperName,
723
+ reactNodeRenderHelperName,
724
+ )];
725
+ }
726
+
727
+ if (node.kind === "fragment") {
728
+ return node.children.flatMap((child) =>
729
+ collectHtmlParts(
730
+ child,
731
+ escapeHelperName,
732
+ escapeBatchHelperName,
733
+ asyncComponentNames,
734
+ dynamicAttributes,
735
+ contextProviderHelperName,
736
+ contextConsumerHelperName,
737
+ reactNodeRenderHelperName,
738
+ ),
739
+ );
740
+ }
741
+
742
+ if (node.kind === "component") {
743
+ if (node.name === "Suspense") {
744
+ return [
745
+ stringLiteral("<!--$-->"),
746
+ ...node.children.flatMap((child) =>
747
+ collectHtmlParts(
748
+ child,
749
+ escapeHelperName,
750
+ escapeBatchHelperName,
751
+ asyncComponentNames,
752
+ dynamicAttributes,
753
+ contextProviderHelperName,
754
+ contextConsumerHelperName,
755
+ reactNodeRenderHelperName,
756
+ ),
757
+ ),
758
+ stringLiteral("<!--/$-->"),
759
+ ];
760
+ }
761
+
762
+ if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
763
+ const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
764
+ return [
765
+ `${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
766
+ ];
767
+ }
768
+
769
+ if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
770
+ const renderProp = findComponentRenderProp(node.props, "children");
771
+
772
+ if (renderProp !== undefined) {
773
+ const valueName = renderProp.valueName ?? "_value";
774
+ return [
775
+ `${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
776
+ ];
777
+ }
778
+ }
779
+
780
+ if (isClientBoundaryPlaceholder(node)) {
781
+ const helperName = currentClientBoundaryHelperName;
782
+ if (helperName !== undefined) {
783
+ return [
784
+ `${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(
785
+ node.props,
786
+ node.children,
787
+ escapeHelperName,
788
+ escapeBatchHelperName,
789
+ asyncComponentNames,
790
+ dynamicAttributes,
791
+ contextProviderHelperName,
792
+ contextConsumerHelperName,
793
+ reactNodeRenderHelperName,
794
+ )})`,
795
+ ];
796
+ }
797
+
798
+ return [stringLiteral(clientBoundaryPlaceholder(node))];
799
+ }
800
+
801
+ if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
802
+ return [
803
+ `${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(
804
+ node.props,
805
+ node.children,
806
+ escapeHelperName,
807
+ escapeBatchHelperName,
808
+ asyncComponentNames,
809
+ dynamicAttributes,
810
+ contextProviderHelperName,
811
+ contextConsumerHelperName,
812
+ reactNodeRenderHelperName,
813
+ )})`,
814
+ ];
815
+ }
816
+
817
+ return [
818
+ emitComponentCallExpression(
819
+ node.name,
820
+ emitPropsObject(
821
+ node.props,
822
+ node.children,
823
+ escapeHelperName,
824
+ escapeBatchHelperName,
825
+ asyncComponentNames,
826
+ dynamicAttributes,
827
+ contextProviderHelperName,
828
+ contextConsumerHelperName,
829
+ reactNodeRenderHelperName,
830
+ ),
831
+ asyncComponentNames,
832
+ ),
833
+ ];
834
+ }
835
+
836
+ if (node.kind === "async-boundary") {
837
+ return [];
838
+ }
839
+
840
+ const closeTag = `</${node.tagName}>`;
841
+
842
+ if (node.tagName === "textarea") {
843
+ const attributeScan = scanElementAttributes(node.tagName, node.attributes);
844
+ return [
845
+ stringLiteral("<textarea"),
846
+ ...collectElementAttributeParts(
847
+ node.tagName,
848
+ node.attributes,
849
+ escapeHelperName,
850
+ escapeBatchHelperName,
851
+ dynamicAttributes,
852
+ attributeScan,
853
+ ),
854
+ stringLiteral(">"),
855
+ ...collectTextareaValueParts(
856
+ node,
857
+ escapeHelperName,
858
+ escapeBatchHelperName,
859
+ asyncComponentNames,
860
+ dynamicAttributes,
861
+ contextProviderHelperName,
862
+ contextConsumerHelperName,
863
+ reactNodeRenderHelperName,
864
+ attributeScan,
865
+ ),
866
+ stringLiteral(closeTag),
867
+ ];
868
+ }
869
+
870
+ const childrenExpression = emitBatchedSimpleChildrenExpression(
871
+ node.children,
872
+ escapeBatchHelperName,
873
+ );
874
+ const attributeScan = scanElementAttributes(node.tagName, node.attributes);
875
+ const childSelectedValueCode = node.tagName === "select"
876
+ ? attributeScan.formValueAttributeCode
877
+ : undefined;
878
+ const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
879
+ const dangerousInnerHtml = emitDangerouslySetInnerHtmlExpression(node.attributes);
880
+ const childrenParts =
881
+ dangerousInnerHtml !== undefined
882
+ ? [dangerousInnerHtml]
883
+ : childrenExpression === undefined || childSelectedValueCode !== undefined
884
+ ? node.children.flatMap((child) =>
885
+ collectHtmlParts(
886
+ child,
887
+ escapeHelperName,
888
+ escapeBatchHelperName,
889
+ asyncComponentNames,
890
+ dynamicAttributes,
891
+ contextProviderHelperName,
892
+ contextConsumerHelperName,
893
+ reactNodeRenderHelperName,
894
+ childSelectedValueCode,
895
+ ),
896
+ )
897
+ : [childrenExpression];
898
+
899
+ return [
900
+ stringLiteral(`<${node.tagName}`),
901
+ ...collectElementAttributeParts(
902
+ node.tagName,
903
+ node.attributes,
904
+ escapeHelperName,
905
+ escapeBatchHelperName,
906
+ dynamicAttributes,
907
+ attributeScan,
908
+ ),
909
+ ...(selectedAttributePart === undefined ? [] : [selectedAttributePart]),
910
+ stringLiteral(">"),
911
+ ...childrenParts,
912
+ stringLiteral(closeTag),
913
+ ];
914
+ }
915
+
916
+ function emitDangerouslySetInnerHtmlExpression(attrs: readonly AttributeIr[]): string | undefined {
917
+ const attr = attrs.find(
918
+ (candidate): candidate is Extract<AttributeIr, { kind: "dynamic-attr" }> =>
919
+ candidate.kind === "dynamic-attr" && candidate.name === "dangerouslySetInnerHTML",
920
+ );
921
+
922
+ if (attr === undefined) {
923
+ return undefined;
924
+ }
925
+
926
+ return `(() => { const _value = (${attr.code}); return typeof _value === "object" && _value !== null && typeof _value.__html === "string" ? _value.__html : ""; })()`;
927
+ }
928
+
929
+ function collectHtmlAttributeParts(
930
+ tagName: string,
931
+ attr: AttributeIr,
932
+ escapeHelperName: string,
933
+ escapeBatchHelperName: string | undefined,
934
+ dynamicAttributes: "drop" | "emit",
935
+ ): string[] {
936
+ if (attr.kind === "spread-attr") {
937
+ return dynamicAttributes === "drop"
938
+ ? []
939
+ : [`${currentSpreadAttributesHelperName}(${stringLiteral(tagName)}, (${attr.code}))`];
940
+ }
941
+
942
+ if (attr.kind === "event" || attr.name === "key" || attr.name === "dangerouslySetInnerHTML") {
943
+ return [];
944
+ }
945
+
946
+ const htmlName = htmlAttributeNameForElement(tagName, attr.name);
947
+
948
+ if (attr.kind === "static-attr") {
949
+ // Reject literal `javascript:` / `data:` / etc. in JSX source. This
950
+ // never produces a runtime branch because the value is known at
951
+ // compile time -- we just drop the attribute (matching the dynamic
952
+ // path) so a developer cannot statically introduce the same XSS.
953
+ if (isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value)) {
954
+ return [];
955
+ }
956
+ // Issue 077: a literal string value for `srcdoc` etc. can never be
957
+ // the `{ __html: ... }` opt-in shape, so it is dropped at compile
958
+ // time.
959
+ if (isDangerousHtmlAttribute(htmlName)) {
960
+ return [];
961
+ }
962
+ return [`${stringLiteral(` ${htmlName}="${escapeHtml(attr.value)}"`)}`];
963
+ }
964
+
965
+ if (dynamicAttributes === "drop") {
966
+ return [];
967
+ }
968
+
969
+ if (attr.name === "style") {
970
+ return [emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName)];
971
+ }
972
+
973
+ if (isDangerousHtmlAttribute(htmlName)) {
974
+ // Dynamic srcdoc must arrive as `{ __html: "..." }`. Drop anything
975
+ // else at runtime so a value computed from a loader cannot inject
976
+ // executable HTML into the iframe document.
977
+ return [
978
+ `(() => { const _value = (${attr.code}); if (_value == null || _value === false) return ""; if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") return ${stringLiteral(` ${htmlName}="`)} + ${escapeHelperName}(_value.__html) + ${stringLiteral("\"")}; return ""; })()`,
979
+ ];
980
+ }
981
+
982
+ return [emitDynamicAttributeExpression(htmlName, attr.code, escapeHelperName)];
983
+ }
984
+
985
+ function collectElementAttributeParts(
986
+ tagName: string,
987
+ attrs: readonly AttributeIr[],
988
+ escapeHelperName: string,
989
+ escapeBatchHelperName: string | undefined,
990
+ dynamicAttributes: "drop" | "emit",
991
+ attributeScan = scanElementAttributes(tagName, attrs),
992
+ ): string[] {
993
+ return attrs.flatMap((attr) =>
994
+ attr.kind !== "spread-attr" &&
995
+ ((tagName === "input" &&
996
+ ((attr.name === "defaultValue" && attributeScan.hasExplicitInputValue) ||
997
+ (attr.name === "defaultChecked" && attributeScan.hasExplicitInputChecked))) ||
998
+ ((tagName === "textarea" || tagName === "select") &&
999
+ (attr.name === "value" || attr.name === "defaultValue")))
1000
+ ? []
1001
+ : collectHtmlAttributeParts(
1002
+ tagName,
1003
+ attr,
1004
+ escapeHelperName,
1005
+ escapeBatchHelperName,
1006
+ dynamicAttributes,
1007
+ ),
1008
+ );
1009
+ }
1010
+
1011
+ interface ElementAttributeScan {
1012
+ hasExplicitInputValue: boolean;
1013
+ hasExplicitInputChecked: boolean;
1014
+ formValueAttributeCode: string | undefined;
1015
+ }
1016
+
1017
+ function scanElementAttributes(
1018
+ tagName: string,
1019
+ attrs: readonly AttributeIr[],
1020
+ ): ElementAttributeScan {
1021
+ let hasExplicitInputValue = false;
1022
+ let hasExplicitInputChecked = false;
1023
+ let valueAttributeCode: string | undefined;
1024
+ let defaultValueAttributeCode: string | undefined;
1025
+
1026
+ for (const attr of attrs) {
1027
+ if (attr.kind === "spread-attr") {
1028
+ continue;
1029
+ }
1030
+
1031
+ if (tagName === "input") {
1032
+ if (attr.name === "value") {
1033
+ hasExplicitInputValue = true;
1034
+ } else if (attr.name === "checked") {
1035
+ hasExplicitInputChecked = true;
1036
+ }
1037
+ }
1038
+
1039
+ if ((tagName === "textarea" || tagName === "select") && attr.name === "value") {
1040
+ valueAttributeCode = readFormValueAttributeCode(attr);
1041
+ } else if (
1042
+ (tagName === "textarea" || tagName === "select") &&
1043
+ attr.name === "defaultValue"
1044
+ ) {
1045
+ defaultValueAttributeCode = readFormValueAttributeCode(attr);
1046
+ }
1047
+ }
1048
+
1049
+ return {
1050
+ hasExplicitInputValue,
1051
+ hasExplicitInputChecked,
1052
+ formValueAttributeCode: valueAttributeCode ?? defaultValueAttributeCode,
1053
+ };
1054
+ }
1055
+
1056
+ function readFormValueAttributeCode(
1057
+ attr: Exclude<AttributeIr, { kind: "spread-attr" }>,
1058
+ ): string | undefined {
1059
+ if (attr.kind === "event") {
1060
+ return undefined;
1061
+ }
1062
+
1063
+ return attr.kind === "static-attr" ? stringLiteral(attr.value) : `(${attr.code})`;
1064
+ }
1065
+
1066
+ function emitDynamicAttributeExpression(
1067
+ name: string,
1068
+ code: string,
1069
+ escapeHelperName: string,
1070
+ ): string {
1071
+ if (isUrlAttribute(name)) {
1072
+ // Run the value through the inline URL safety helper. The helper
1073
+ // returns the value when safe and `undefined` when the attribute
1074
+ // should be dropped. Using an IIFE here is necessary because we
1075
+ // need to capture the value once and branch on the helper output.
1076
+ return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; const _checked = ${currentUrlSafeHelperName}(${stringLiteral(name)}, _value === true ? "" : _value); return _checked === undefined ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_checked) + ${stringLiteral("\"")}; })()`;
1077
+ }
1078
+
1079
+ const inlineExpr = simpleSideEffectFreeExpression(code);
1080
+
1081
+ if (inlineExpr !== undefined) {
1082
+ // Inline 3 evaluations to avoid per-attribute IIFE closure allocation.
1083
+ // Safe because `simpleSideEffectFreeExpression` only matches expressions
1084
+ // whose evaluation has no observable side effects (identifier read,
1085
+ // member chain, literal, this).
1086
+ return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral("\"")})`;
1087
+ }
1088
+
1089
+ return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral("\"")}; })()`;
1090
+ }
1091
+
1092
+ function emitDynamicStyleAttributeExpression(
1093
+ code: string,
1094
+ escapeHelperName: string,
1095
+ escapeBatchHelperName: string | undefined,
1096
+ ): string {
1097
+ const staticStyleExpression = emitStaticStyleObjectAttributeExpression(code, escapeHelperName);
1098
+
1099
+ if (staticStyleExpression !== undefined) {
1100
+ return staticStyleExpression;
1101
+ }
1102
+
1103
+ const escapedPair = escapeBatchHelperName === undefined
1104
+ ? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
1105
+ : `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
1106
+
1107
+ return `(() => { const _value = (${code}); if (_value == null || _value === false) return ""; if (typeof _value === "string") { const _style = ${escapeHelperName}(_value); return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; } const _style = Object.entries(_value).filter(([, _styleValue]) => _styleValue != null && _styleValue !== false).map(([_styleName, _styleValue]) => { const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase()); return ${escapedPair}; }).join(";"); return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
1108
+ }
1109
+
1110
+ function emitStaticStyleObjectAttributeExpression(
1111
+ code: string,
1112
+ escapeHelperName: string,
1113
+ ): string | undefined {
1114
+ const entries = parseStaticStyleObjectLiteral(code);
1115
+
1116
+ if (entries === undefined) {
1117
+ return undefined;
1118
+ }
1119
+
1120
+ if (entries.length === 0) {
1121
+ return `""`;
1122
+ }
1123
+
1124
+ // Stage B — all values are compile-time literals: collapse to a single
1125
+ // constant string. null/false entries are dropped at build time.
1126
+ const literalEntries = entries.map((entry) => ({
1127
+ cssName: entry.cssName,
1128
+ literal: parseStyleLiteralValue(entry.valueCode),
1129
+ }));
1130
+
1131
+ if (literalEntries.every((entry) => entry.literal !== undefined)) {
1132
+ const parts = literalEntries
1133
+ .filter((entry) => entry.literal !== null)
1134
+ .map((entry) => `${entry.cssName}:${escapeHtml(String(entry.literal))}`);
1135
+
1136
+ if (parts.length === 0) {
1137
+ return `""`;
1138
+ }
1139
+
1140
+ return stringLiteral(` style="${parts.join(";")}"`);
1141
+ }
1142
+
1143
+ // Stage A — needSep tracking with inline string accumulator, no intermediate
1144
+ // array allocation and no `.join(";")` per render.
1145
+ const statements = entries.map((entry) =>
1146
+ `{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`
1147
+ );
1148
+
1149
+ return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
1150
+ }
1151
+
1152
+ function collectTextareaValueParts(
1153
+ node: Extract<JsxNodeIr, { kind: "element" }>,
1154
+ escapeHelperName: string,
1155
+ escapeBatchHelperName: string | undefined,
1156
+ asyncComponentNames: ReadonlySet<string>,
1157
+ dynamicAttributes: "drop" | "emit",
1158
+ contextProviderHelperName?: string,
1159
+ contextConsumerHelperName?: string,
1160
+ reactNodeRenderHelperName?: string,
1161
+ attributeScan = scanElementAttributes(node.tagName, node.attributes),
1162
+ ): string[] {
1163
+ const valueCode = attributeScan.formValueAttributeCode;
1164
+ if (valueCode !== undefined) {
1165
+ return [`${escapeHelperName}(${valueCode})`];
1166
+ }
1167
+
1168
+ return node.children.flatMap((child) =>
1169
+ collectHtmlParts(
1170
+ child,
1171
+ escapeHelperName,
1172
+ escapeBatchHelperName,
1173
+ asyncComponentNames,
1174
+ dynamicAttributes,
1175
+ contextProviderHelperName,
1176
+ contextConsumerHelperName,
1177
+ reactNodeRenderHelperName,
1178
+ )
1179
+ );
1180
+ }
1181
+
1182
+ function collectOptionSelectedAttributePart(
1183
+ node: Extract<JsxNodeIr, { kind: "element" }>,
1184
+ selectedValueCode: string | undefined,
1185
+ ): string | undefined {
1186
+ if (selectedValueCode === undefined || node.tagName !== "option") {
1187
+ return undefined;
1188
+ }
1189
+
1190
+ const optionValueCode = findOptionValueCode(node);
1191
+ if (optionValueCode === undefined) {
1192
+ return undefined;
1193
+ }
1194
+
1195
+ return `(() => { const _selected = (${selectedValueCode}); return _selected == null ? "" : String(_selected) === String(${optionValueCode}) ? ${stringLiteral(' selected=""')} : ""; })()`;
1196
+ }
1197
+
1198
+ function findOptionValueCode(node: Extract<JsxNodeIr, { kind: "element" }>): string | undefined {
1199
+ const valueAttr = node.attributes.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
1200
+ if (valueAttr !== undefined && valueAttr.kind !== "event" && valueAttr.kind !== "spread-attr") {
1201
+ return valueAttr.kind === "static-attr" ? stringLiteral(valueAttr.value) : `(${valueAttr.code})`;
1202
+ }
1203
+
1204
+ return node.children.every((child) => child.kind === "text")
1205
+ ? stringLiteral(node.children.map((child) => child.value).join(""))
1206
+ : undefined;
1207
+ }
1208
+
1209
+ function htmlAttributeNameForElement(tagName: string, name: string): string {
1210
+ if (tagName === "input") {
1211
+ if (name === "defaultValue") {
1212
+ return "value";
1213
+ }
1214
+
1215
+ if (name === "defaultChecked") {
1216
+ return "checked";
1217
+ }
1218
+ }
1219
+
1220
+ return htmlAttributeName(name);
1221
+ }
1222
+
1223
+ function rawHtmlExpression(code: string): string {
1224
+ return `(() => { const _value = (${code}); return Array.isArray(_value) ? _value.join("") : String(_value ?? ""); })()`;
1225
+ }
1226
+
1227
+ function emitBatchedSimpleChildrenExpression(
1228
+ children: readonly JsxNodeIr[],
1229
+ escapeBatchHelperName: string | undefined,
1230
+ ): string | undefined {
1231
+ if (escapeBatchHelperName === undefined) {
1232
+ return undefined;
1233
+ }
1234
+
1235
+ const dynamicChildren = children.filter(
1236
+ (child) => child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node",
1237
+ ) as Array<Extract<JsxNodeIr, { kind: "expr" }>>;
1238
+
1239
+ if (dynamicChildren.length < 2) {
1240
+ return undefined;
1241
+ }
1242
+
1243
+ if (
1244
+ children.some(
1245
+ (child) =>
1246
+ child.kind !== "text" &&
1247
+ !(child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node"),
1248
+ )
1249
+ ) {
1250
+ return undefined;
1251
+ }
1252
+
1253
+ const values = dynamicChildren.map((child) => child.code);
1254
+ let dynamicIndex = 0;
1255
+ const pieces = children.map((child) => {
1256
+ if (child.kind === "text") {
1257
+ return stringLiteral(escapeHtml(child.value));
1258
+ }
1259
+
1260
+ const index = dynamicIndex;
1261
+ dynamicIndex += 1;
1262
+
1263
+ return `_escaped[${index}]`;
1264
+ });
1265
+
1266
+ return `(() => { const _escaped = ${escapeBatchHelperName}([${values.join(", ")}]); return ${pieces.join(" + ")}; })()`;
1267
+ }
1268
+
1269
+ function emitHtmlExpressionFromChildren(
1270
+ children: JsxNodeIr[],
1271
+ escapeHelperName: string,
1272
+ escapeBatchHelperName: string | undefined,
1273
+ asyncComponentNames: ReadonlySet<string>,
1274
+ dynamicAttributes: "drop" | "emit",
1275
+ contextProviderHelperName?: string,
1276
+ contextConsumerHelperName?: string,
1277
+ reactNodeRenderHelperName?: string,
1278
+ ): string {
1279
+ if (children.length === 0) {
1280
+ return "\"\"";
1281
+ }
1282
+
1283
+ return emitHtmlExpression(
1284
+ { kind: "fragment", children },
1285
+ escapeHelperName,
1286
+ escapeBatchHelperName,
1287
+ asyncComponentNames,
1288
+ dynamicAttributes,
1289
+ contextProviderHelperName,
1290
+ contextConsumerHelperName,
1291
+ reactNodeRenderHelperName,
1292
+ );
1293
+ }
1294
+
1295
+ function emitSyncListIife(
1296
+ node: Extract<JsxNodeIr, { kind: "list" }>,
1297
+ escapeHelperName: string,
1298
+ escapeBatchHelperName: string | undefined,
1299
+ asyncComponentNames: ReadonlySet<string>,
1300
+ dynamicAttributes: "drop" | "emit",
1301
+ contextProviderHelperName?: string,
1302
+ contextConsumerHelperName?: string,
1303
+ reactNodeRenderHelperName?: string,
1304
+ ): string {
1305
+ const valueExpression = emitHtmlExpressionFromChildren(
1306
+ node.children,
1307
+ escapeHelperName,
1308
+ escapeBatchHelperName,
1309
+ asyncComponentNames,
1310
+ dynamicAttributes,
1311
+ contextProviderHelperName,
1312
+ contextConsumerHelperName,
1313
+ reactNodeRenderHelperName,
1314
+ );
1315
+ const itemBinding = `const ${node.itemName} = _arr[_i];`;
1316
+ const indexBinding =
1317
+ node.indexName === undefined ? "" : ` const ${node.indexName} = _i;`;
1318
+ const bodyStatements =
1319
+ node.bodyStatements === undefined || node.bodyStatements.length === 0
1320
+ ? ""
1321
+ : ` ${node.bodyStatements.join(" ")}`;
1322
+
1323
+ return `(() => { let _o = ""; const _arr = (${node.itemsCode}); for (let _i = 0, _len = _arr.length; _i < _len; _i++) { ${itemBinding}${indexBinding}${bodyStatements} _o += ${valueExpression}; } return _o; })()`;
1324
+ }
1325
+
1326
+ function emitListRenderer(
1327
+ node: Extract<JsxNodeIr, { kind: "list" }>,
1328
+ parameters: string,
1329
+ escapeHelperName: string,
1330
+ escapeBatchHelperName: string | undefined,
1331
+ asyncComponentNames: ReadonlySet<string>,
1332
+ dynamicAttributes: "drop" | "emit",
1333
+ contextProviderHelperName?: string,
1334
+ contextConsumerHelperName?: string,
1335
+ reactNodeRenderHelperName?: string,
1336
+ ): string {
1337
+ const valueExpression = emitHtmlExpressionFromChildren(
1338
+ node.children,
1339
+ escapeHelperName,
1340
+ escapeBatchHelperName,
1341
+ asyncComponentNames,
1342
+ dynamicAttributes,
1343
+ contextProviderHelperName,
1344
+ contextConsumerHelperName,
1345
+ reactNodeRenderHelperName,
1346
+ );
1347
+ const asyncKeyword = containsAsyncServerOperationInChildren(
1348
+ node.children,
1349
+ asyncComponentNames,
1350
+ )
1351
+ ? "async "
1352
+ : "";
1353
+
1354
+ if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
1355
+ return `${asyncKeyword}(${parameters}) => ${valueExpression}`;
1356
+ }
1357
+
1358
+ return `${asyncKeyword}(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
1359
+ }
1360
+
1361
+ function emitPropsObject(
1362
+ props: ComponentPropIr[],
1363
+ children: JsxNodeIr[] = [],
1364
+ escapeHelperName: string,
1365
+ escapeBatchHelperName: string | undefined,
1366
+ asyncComponentNames: ReadonlySet<string>,
1367
+ dynamicAttributes: "drop" | "emit",
1368
+ contextProviderHelperName?: string,
1369
+ contextConsumerHelperName?: string,
1370
+ reactNodeRenderHelperName?: string,
1371
+ ): string {
1372
+ const entries = props.map((prop) => {
1373
+ if (prop.kind === "spread-prop") {
1374
+ return `...(${prop.code})`;
1375
+ }
1376
+
1377
+ if (prop.kind === "render-prop") {
1378
+ return `${emitPropName(prop.name)}: ${emitHtmlExpressionFromChildren(prop.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`;
1379
+ }
1380
+
1381
+ return `${emitPropName(prop.name)}: (${prop.code})`;
1382
+ });
1383
+
1384
+ if (children.length > 0) {
1385
+ entries.push(
1386
+ `children: ${emitHtmlExpressionFromChildren(children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`,
1387
+ );
1388
+ }
1389
+
1390
+ return `{ ${entries.join(", ")} }`;
1391
+ }
1392
+
1393
+ function emitComponentCallExpression(
1394
+ name: string,
1395
+ propsCode: string,
1396
+ asyncComponentNames: ReadonlySet<string>,
1397
+ ): string {
1398
+ const call = `${name}(${propsCode})`;
1399
+ return asyncComponentNames.has(name) ? `(await ${call})` : call;
1400
+ }
1401
+
1402
+ function isClientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
1403
+ return node.clientReference !== undefined && !isCompatClientReference(node);
1404
+ }
1405
+
1406
+ function isCompatClientReference(node: Extract<JsxNodeIr, { kind: "component" }>): boolean {
1407
+ return node.clientReference !== undefined && /\.(?:compat)\.[cm]?[jt]sx?$/.test(node.clientReference.moduleId);
1408
+ }
1409
+
1410
+ function clientBoundaryPlaceholder(node: Extract<JsxNodeIr, { kind: "component" }>): string {
1411
+ return `<!--mreact-client-boundary:${escapeHtml(node.name)}-->`;
1412
+ }
1413
+
1414
+ function collectAsyncServerComponentNames(components: readonly ComponentIr[]): Set<string> {
1415
+ const names = new Set(
1416
+ components
1417
+ .filter((component) => component.async === true)
1418
+ .map((component) => component.name),
1419
+ );
1420
+
1421
+ let changed = true;
1422
+
1423
+ while (changed) {
1424
+ changed = false;
1425
+
1426
+ for (const component of components) {
1427
+ if (
1428
+ !names.has(component.name) &&
1429
+ containsAsyncServerOperation(component.root, names)
1430
+ ) {
1431
+ names.add(component.name);
1432
+ changed = true;
1433
+ }
1434
+ }
1435
+ }
1436
+
1437
+ return names;
1438
+ }
1439
+
1440
+ function containsAsyncServerOperationInChildren(
1441
+ children: readonly JsxNodeIr[],
1442
+ asyncComponentNames: ReadonlySet<string>,
1443
+ ): boolean {
1444
+ return children.some((child) => containsAsyncServerOperation(child, asyncComponentNames));
1445
+ }
1446
+
1447
+ function containsAsyncServerOperation(
1448
+ node: JsxNodeIr,
1449
+ asyncComponentNames: ReadonlySet<string>,
1450
+ ): boolean {
1451
+ if (node.kind === "async-boundary") {
1452
+ return true;
1453
+ }
1454
+
1455
+ if (node.kind === "component") {
1456
+ return (
1457
+ asyncComponentNames.has(node.name) ||
1458
+ containsAsyncServerOperationInChildren(node.children, asyncComponentNames) ||
1459
+ node.props.some(
1460
+ (prop) =>
1461
+ prop.kind === "render-prop" &&
1462
+ containsAsyncServerOperationInChildren(prop.children, asyncComponentNames),
1463
+ )
1464
+ );
1465
+ }
1466
+
1467
+ if (node.kind === "conditional") {
1468
+ return containsAsyncServerOperationInChildren(
1469
+ [...node.whenTrue, ...node.whenFalse],
1470
+ asyncComponentNames,
1471
+ );
1472
+ }
1473
+
1474
+ if (node.kind === "list") {
1475
+ return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
1476
+ }
1477
+
1478
+ if (node.kind === "element" || node.kind === "fragment") {
1479
+ return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
1480
+ }
1481
+
1482
+ return false;
1483
+ }
1484
+
1485
+ function emitPropName(name: string): string {
1486
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
1487
+ }
1488
+
1489
+ function allocateEscapeHelperName(ir: ModuleIr): string {
1490
+ return allocateHelperName(ir, "_escapeHtml");
1491
+ }
1492
+
1493
+ function allocateHelperName(ir: ModuleIr, baseName: string): string {
1494
+ const reservedNames = new Set<string>(ir.moduleBindingNames);
1495
+
1496
+ for (const component of ir.components) {
1497
+ reservedNames.add(component.name);
1498
+
1499
+ for (const bindingName of component.bindingNames) {
1500
+ reservedNames.add(bindingName);
1501
+ }
1502
+ }
1503
+
1504
+ let name = baseName;
1505
+ let index = 1;
1506
+
1507
+ while (reservedNames.has(name)) {
1508
+ name = `${baseName}$${index}`;
1509
+ index += 1;
1510
+ }
1511
+
1512
+ return name;
1513
+ }
1514
+
1515
+ function usesContextProvider(ir: ModuleIr): boolean {
1516
+ return ir.components.some((component) => containsContextProvider(component.root));
1517
+ }
1518
+
1519
+ function usesContextConsumer(ir: ModuleIr): boolean {
1520
+ return ir.components.some((component) => containsContextConsumer(component.root));
1521
+ }
1522
+
1523
+ function usesReactNodeRender(ir: ModuleIr): boolean {
1524
+ return ir.components.some(
1525
+ (component) =>
1526
+ containsReactNodeRender(component.root) ||
1527
+ component.bodyStatements.some((statement) =>
1528
+ statement.includes(oxcServerStringReactNodeRenderHelperPlaceholder),
1529
+ ),
1530
+ );
1531
+ }
1532
+
1533
+ function replaceOxcServerStringReactNodeRenderHelper(
1534
+ code: string,
1535
+ helperName: string | undefined,
1536
+ ): string {
1537
+ return helperName === undefined
1538
+ ? code
1539
+ : code.replaceAll(oxcServerStringReactNodeRenderHelperPlaceholder, helperName);
1540
+ }
1541
+
1542
+ function usesClientBoundary(ir: ModuleIr): boolean {
1543
+ return ir.components.some((component) => containsClientBoundary(component.root));
1544
+ }
1545
+
1546
+ function emitClientBoundaryHelper(name: string): string {
1547
+ const propsHelperName = `${name}$hasNonSerializableProps`;
1548
+
1549
+ return [
1550
+ `function ${propsHelperName}(value) {`,
1551
+ ` if (typeof value === "function" || typeof value === "symbol") return true;`,
1552
+ ` if (value === null || typeof value !== "object") return false;`,
1553
+ ` if (Array.isArray(value)) return value.some(${propsHelperName});`,
1554
+ ` for (const key of Object.keys(value)) {`,
1555
+ ` if (${propsHelperName}(value[key])) return true;`,
1556
+ ` }`,
1557
+ ` return false;`,
1558
+ `}`,
1559
+ `function ${name}(name, props) {`,
1560
+ ` const _name = String(name);`,
1561
+ ` const _escapedName = _name.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");`,
1562
+ ` const _props = props ?? {};`,
1563
+ ` const _nonSerializable = ${propsHelperName}(_props);`,
1564
+ ` const _nonSerializableAttr = _nonSerializable ? ' data-mreact-client-boundary-nonserializable="true"' : "";`,
1565
+ ` const _json = (JSON.stringify(_props) ?? "{}").replaceAll("<", "\\\\u003c");`,
1566
+ ` return \`<template data-mreact-client-boundary="\${_escapedName}"\${_nonSerializableAttr}></template><script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
1567
+ `}`,
1568
+ ].join("\n");
1569
+ }
1570
+
1571
+ function emitSpreadAttributesHelper(
1572
+ name: string,
1573
+ escapeHelperName: string,
1574
+ urlSafeHelperName: string,
1575
+ ): string {
1576
+ const aliases = JSON.stringify({
1577
+ acceptCharset: "accept-charset",
1578
+ autoFocus: "autofocus",
1579
+ autoPlay: "autoplay",
1580
+ charSet: "charset",
1581
+ className: "class",
1582
+ colSpan: "colspan",
1583
+ contentEditable: "contenteditable",
1584
+ crossOrigin: "crossorigin",
1585
+ encType: "enctype",
1586
+ formAction: "formaction",
1587
+ frameBorder: "frameborder",
1588
+ htmlFor: "for",
1589
+ httpEquiv: "http-equiv",
1590
+ maxLength: "maxlength",
1591
+ minLength: "minlength",
1592
+ noValidate: "novalidate",
1593
+ playsInline: "playsinline",
1594
+ readOnly: "readonly",
1595
+ rowSpan: "rowspan",
1596
+ spellCheck: "spellcheck",
1597
+ srcDoc: "srcdoc",
1598
+ srcSet: "srcset",
1599
+ tabIndex: "tabindex",
1600
+ useMap: "usemap",
1601
+ });
1602
+ const urlAttributes = JSON.stringify([
1603
+ "href",
1604
+ "src",
1605
+ "action",
1606
+ "formaction",
1607
+ "xlink:href",
1608
+ "ping",
1609
+ "poster",
1610
+ "background",
1611
+ "manifest",
1612
+ ]);
1613
+ const dangerousAttributes = JSON.stringify(["srcdoc"]);
1614
+
1615
+ return [
1616
+ `const ${name}$aliases = ${aliases};`,
1617
+ `const ${name}$urlAttributes = new Set(${urlAttributes});`,
1618
+ `const ${name}$dangerousAttributes = new Set(${dangerousAttributes});`,
1619
+ `function ${name}$style(value) {`,
1620
+ ` if (value == null || value === false) return "";`,
1621
+ ` if (typeof value === "string") return value;`,
1622
+ ` let _style = "";`,
1623
+ ` for (const _styleName of Object.keys(value)) {`,
1624
+ ` const _styleValue = value[_styleName];`,
1625
+ ` if (_styleValue == null || _styleValue === false) continue;`,
1626
+ ` const _cssName = String(_styleName).startsWith("--") ? String(_styleName) : String(_styleName).replace(/[A-Z]/g, (_char) => "-" + _char.toLowerCase());`,
1627
+ ` _style += (_style === "" ? "" : ";") + _cssName + ":" + (_styleValue === true ? "" : String(_styleValue));`,
1628
+ ` }`,
1629
+ ` return _style;`,
1630
+ `}`,
1631
+ `function ${name}(tagName, props) {`,
1632
+ ` if (props == null || props === false) return "";`,
1633
+ ` let _out = "";`,
1634
+ ` for (const _rawName of Object.keys(props)) {`,
1635
+ ` let _value = props[_rawName];`,
1636
+ ` if (_value == null || _value === false) continue;`,
1637
+ ` if (_rawName === "key" || _rawName === "ref" || _rawName === "children") continue;`,
1638
+ ` if (/^on[A-Za-z]/.test(_rawName)) continue;`,
1639
+ ` let _name = tagName === "input" && _rawName === "defaultValue" ? "value" : tagName === "input" && _rawName === "defaultChecked" ? "checked" : (${name}$aliases[_rawName] ?? _rawName);`,
1640
+ ` if (!/^[A-Za-z_:][A-Za-z0-9:_.-]*$/.test(_name)) continue;`,
1641
+ ` if (_name === "style") {`,
1642
+ ` const _style = ${name}$style(_value);`,
1643
+ ` if (_style !== "") _out += " style=\\"" + ${escapeHelperName}(_style) + "\\"";`,
1644
+ ` continue;`,
1645
+ ` }`,
1646
+ ` if (${name}$dangerousAttributes.has(_name)) {`,
1647
+ ` if (typeof _value === "object" && _value !== null && typeof _value.__html === "string") {`,
1648
+ ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value.__html) + "\\"";`,
1649
+ ` }`,
1650
+ ` continue;`,
1651
+ ` }`,
1652
+ ` if (${name}$urlAttributes.has(_name)) {`,
1653
+ ` _value = ${urlSafeHelperName}(_name, _value === true ? "" : _value);`,
1654
+ ` if (_value === undefined) continue;`,
1655
+ ` }`,
1656
+ ` _out += " " + _name + "=\\"" + ${escapeHelperName}(_value === true ? "" : _value) + "\\"";`,
1657
+ ` }`,
1658
+ ` return _out;`,
1659
+ `}`,
1660
+ ].join("\n");
1661
+ }
1662
+
1663
+ function containsClientBoundary(node: JsxNodeIr): boolean {
1664
+ if (node.kind === "component" && isClientBoundaryPlaceholder(node)) {
1665
+ return true;
1666
+ }
1667
+
1668
+ if (node.kind === "conditional") {
1669
+ return [...node.whenTrue, ...node.whenFalse].some(containsClientBoundary);
1670
+ }
1671
+
1672
+ if (node.kind === "list") {
1673
+ return node.children.some(containsClientBoundary);
1674
+ }
1675
+
1676
+ if (node.kind === "fragment" || node.kind === "element") {
1677
+ return node.children.some(containsClientBoundary);
1678
+ }
1679
+
1680
+ if (node.kind === "component") {
1681
+ return (
1682
+ node.children.some(containsClientBoundary) ||
1683
+ node.props.some(
1684
+ (prop) => prop.kind === "render-prop" && prop.children.some(containsClientBoundary),
1685
+ )
1686
+ );
1687
+ }
1688
+
1689
+ if (node.kind === "async-boundary") {
1690
+ return (
1691
+ node.children.some(containsClientBoundary) ||
1692
+ node.placeholderChildren?.some(containsClientBoundary) === true ||
1693
+ node.catchChildren?.some(containsClientBoundary) === true
1694
+ );
1695
+ }
1696
+
1697
+ return false;
1698
+ }
1699
+
1700
+ function containsReactNodeRender(node: JsxNodeIr): boolean {
1701
+ if (node.kind === "expr") {
1702
+ return node.renderMode === "react-node";
1703
+ }
1704
+
1705
+ if (node.kind === "conditional") {
1706
+ return [...node.whenTrue, ...node.whenFalse].some(containsReactNodeRender);
1707
+ }
1708
+
1709
+ if (node.kind === "list") {
1710
+ return node.children.some(containsReactNodeRender);
1711
+ }
1712
+
1713
+ if (node.kind === "fragment" || node.kind === "element") {
1714
+ return node.children.some(containsReactNodeRender);
1715
+ }
1716
+
1717
+ if (node.kind === "component") {
1718
+ if (node.runtime === "compat" && !isClientBoundaryPlaceholder(node)) {
1719
+ return true;
1720
+ }
1721
+
1722
+ return (
1723
+ node.children.some(containsReactNodeRender) ||
1724
+ node.props.some(
1725
+ (prop) => prop.kind === "render-prop" && prop.children.some(containsReactNodeRender),
1726
+ )
1727
+ );
1728
+ }
1729
+
1730
+ if (node.kind === "async-boundary") {
1731
+ return (
1732
+ node.children.some(containsReactNodeRender) ||
1733
+ node.placeholderChildren?.some(containsReactNodeRender) === true ||
1734
+ node.catchChildren?.some(containsReactNodeRender) === true
1735
+ );
1736
+ }
1737
+
1738
+ return false;
1739
+ }
1740
+
1741
+ function containsContextProvider(node: JsxNodeIr): boolean {
1742
+ if (node.kind === "component" && node.name.endsWith(".Provider")) {
1743
+ return true;
1744
+ }
1745
+
1746
+ if (node.kind === "conditional") {
1747
+ return [...node.whenTrue, ...node.whenFalse].some(containsContextProvider);
1748
+ }
1749
+
1750
+ if (node.kind === "list") {
1751
+ return node.children.some(containsContextProvider);
1752
+ }
1753
+
1754
+ if (node.kind === "fragment" || node.kind === "element") {
1755
+ return node.children.some(containsContextProvider);
1756
+ }
1757
+
1758
+ if (node.kind === "component") {
1759
+ return (
1760
+ node.children.some(containsContextProvider) ||
1761
+ node.props.some(
1762
+ (prop) => prop.kind === "render-prop" && prop.children.some(containsContextProvider),
1763
+ )
1764
+ );
1765
+ }
1766
+
1767
+ return false;
1768
+ }
1769
+
1770
+ function containsContextConsumer(node: JsxNodeIr): boolean {
1771
+ if (node.kind === "component" && node.name.endsWith(".Consumer")) {
1772
+ return true;
1773
+ }
1774
+
1775
+ if (node.kind === "conditional") {
1776
+ return [...node.whenTrue, ...node.whenFalse].some(containsContextConsumer);
1777
+ }
1778
+
1779
+ if (node.kind === "list") {
1780
+ return node.children.some(containsContextConsumer);
1781
+ }
1782
+
1783
+ if (node.kind === "fragment" || node.kind === "element") {
1784
+ return node.children.some(containsContextConsumer);
1785
+ }
1786
+
1787
+ if (node.kind === "component") {
1788
+ return (
1789
+ node.children.some(containsContextConsumer) ||
1790
+ node.props.some(
1791
+ (prop) => prop.kind === "render-prop" && prop.children.some(containsContextConsumer),
1792
+ )
1793
+ );
1794
+ }
1795
+
1796
+ return false;
1797
+ }
1798
+
1799
+ function findComponentPropCode(
1800
+ props: readonly ComponentPropIr[],
1801
+ name: string,
1802
+ ): string | undefined {
1803
+ for (const prop of props) {
1804
+ if (prop.kind === "prop" && prop.name === name) {
1805
+ return prop.code;
1806
+ }
1807
+ }
1808
+
1809
+ return undefined;
1810
+ }
1811
+
1812
+ function findComponentRenderProp(
1813
+ props: readonly ComponentPropIr[],
1814
+ name: string,
1815
+ ): Extract<ComponentPropIr, { kind: "render-prop" }> | undefined {
1816
+ for (const prop of props) {
1817
+ if (prop.kind === "render-prop" && prop.name === name) {
1818
+ return prop;
1819
+ }
1820
+ }
1821
+
1822
+ return undefined;
1823
+ }
1824
+
1825
+ function stringLiteral(value: string): string {
1826
+ return JSON.stringify(value);
1827
+ }