@reckona/mreact-compiler 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/diagnostics.d.ts +12 -0
  4. package/dist/diagnostics.d.ts.map +1 -0
  5. package/dist/diagnostics.js +82 -0
  6. package/dist/diagnostics.js.map +1 -0
  7. package/dist/emit-client.d.ts +8 -0
  8. package/dist/emit-client.d.ts.map +1 -0
  9. package/dist/emit-client.js +401 -0
  10. package/dist/emit-client.js.map +1 -0
  11. package/dist/emit-compat.d.ts +11 -0
  12. package/dist/emit-compat.d.ts.map +1 -0
  13. package/dist/emit-compat.js +340 -0
  14. package/dist/emit-compat.js.map +1 -0
  15. package/dist/emit-escape-helper.d.ts +2 -0
  16. package/dist/emit-escape-helper.d.ts.map +1 -0
  17. package/dist/emit-escape-helper.js +45 -0
  18. package/dist/emit-escape-helper.js.map +1 -0
  19. package/dist/emit-server-stream.d.ts +18 -0
  20. package/dist/emit-server-stream.d.ts.map +1 -0
  21. package/dist/emit-server-stream.js +1415 -0
  22. package/dist/emit-server-stream.js.map +1 -0
  23. package/dist/emit-server.d.ts +13 -0
  24. package/dist/emit-server.d.ts.map +1 -0
  25. package/dist/emit-server.js +1103 -0
  26. package/dist/emit-server.js.map +1 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/internal.d.ts +30 -0
  32. package/dist/internal.d.ts.map +1 -0
  33. package/dist/internal.js +229 -0
  34. package/dist/internal.js.map +1 -0
  35. package/dist/ir.d.ts +115 -0
  36. package/dist/ir.d.ts.map +1 -0
  37. package/dist/ir.js +2 -0
  38. package/dist/ir.js.map +1 -0
  39. package/dist/oxc-analysis-types.d.ts +2 -0
  40. package/dist/oxc-analysis-types.d.ts.map +1 -0
  41. package/dist/oxc-analysis-types.js +2 -0
  42. package/dist/oxc-analysis-types.js.map +1 -0
  43. package/dist/oxc-await-analysis.d.ts +5 -0
  44. package/dist/oxc-await-analysis.d.ts.map +1 -0
  45. package/dist/oxc-await-analysis.js +109 -0
  46. package/dist/oxc-await-analysis.js.map +1 -0
  47. package/dist/oxc-await-ids.d.ts +3 -0
  48. package/dist/oxc-await-ids.d.ts.map +1 -0
  49. package/dist/oxc-await-ids.js +50 -0
  50. package/dist/oxc-await-ids.js.map +1 -0
  51. package/dist/oxc-await-validation.d.ts +4 -0
  52. package/dist/oxc-await-validation.d.ts.map +1 -0
  53. package/dist/oxc-await-validation.js +47 -0
  54. package/dist/oxc-await-validation.js.map +1 -0
  55. package/dist/oxc-bindings.d.ts +5 -0
  56. package/dist/oxc-bindings.d.ts.map +1 -0
  57. package/dist/oxc-bindings.js +55 -0
  58. package/dist/oxc-bindings.js.map +1 -0
  59. package/dist/oxc-body-lowering.d.ts +12 -0
  60. package/dist/oxc-body-lowering.d.ts.map +1 -0
  61. package/dist/oxc-body-lowering.js +131 -0
  62. package/dist/oxc-body-lowering.js.map +1 -0
  63. package/dist/oxc-child-analysis.d.ts +17 -0
  64. package/dist/oxc-child-analysis.d.ts.map +1 -0
  65. package/dist/oxc-child-analysis.js +292 -0
  66. package/dist/oxc-child-analysis.js.map +1 -0
  67. package/dist/oxc-code-utils.d.ts +3 -0
  68. package/dist/oxc-code-utils.d.ts.map +1 -0
  69. package/dist/oxc-code-utils.js +16 -0
  70. package/dist/oxc-code-utils.js.map +1 -0
  71. package/dist/oxc-component-detection.d.ts +22 -0
  72. package/dist/oxc-component-detection.d.ts.map +1 -0
  73. package/dist/oxc-component-detection.js +188 -0
  74. package/dist/oxc-component-detection.js.map +1 -0
  75. package/dist/oxc-component-props.d.ts +14 -0
  76. package/dist/oxc-component-props.d.ts.map +1 -0
  77. package/dist/oxc-component-props.js +92 -0
  78. package/dist/oxc-component-props.js.map +1 -0
  79. package/dist/oxc-component-references.d.ts +5 -0
  80. package/dist/oxc-component-references.d.ts.map +1 -0
  81. package/dist/oxc-component-references.js +118 -0
  82. package/dist/oxc-component-references.js.map +1 -0
  83. package/dist/oxc-dom-lowering.d.ts +2 -0
  84. package/dist/oxc-dom-lowering.d.ts.map +1 -0
  85. package/dist/oxc-dom-lowering.js +99 -0
  86. package/dist/oxc-dom-lowering.js.map +1 -0
  87. package/dist/oxc-expression-utils.d.ts +5 -0
  88. package/dist/oxc-expression-utils.d.ts.map +1 -0
  89. package/dist/oxc-expression-utils.js +31 -0
  90. package/dist/oxc-expression-utils.js.map +1 -0
  91. package/dist/oxc-jsx-attributes.d.ts +6 -0
  92. package/dist/oxc-jsx-attributes.d.ts.map +1 -0
  93. package/dist/oxc-jsx-attributes.js +88 -0
  94. package/dist/oxc-jsx-attributes.js.map +1 -0
  95. package/dist/oxc-jsx-text.d.ts +2 -0
  96. package/dist/oxc-jsx-text.d.ts.map +1 -0
  97. package/dist/oxc-jsx-text.js +50 -0
  98. package/dist/oxc-jsx-text.js.map +1 -0
  99. package/dist/oxc-nested-lowering.d.ts +8 -0
  100. package/dist/oxc-nested-lowering.d.ts.map +1 -0
  101. package/dist/oxc-nested-lowering.js +182 -0
  102. package/dist/oxc-nested-lowering.js.map +1 -0
  103. package/dist/oxc-node-utils.d.ts +8 -0
  104. package/dist/oxc-node-utils.d.ts.map +1 -0
  105. package/dist/oxc-node-utils.js +41 -0
  106. package/dist/oxc-node-utils.js.map +1 -0
  107. package/dist/oxc-raw-jsx.d.ts +3 -0
  108. package/dist/oxc-raw-jsx.d.ts.map +1 -0
  109. package/dist/oxc-raw-jsx.js +28 -0
  110. package/dist/oxc-raw-jsx.js.map +1 -0
  111. package/dist/oxc-render-values.d.ts +6 -0
  112. package/dist/oxc-render-values.d.ts.map +1 -0
  113. package/dist/oxc-render-values.js +149 -0
  114. package/dist/oxc-render-values.js.map +1 -0
  115. package/dist/oxc-runtime-emit.d.ts +4 -0
  116. package/dist/oxc-runtime-emit.d.ts.map +1 -0
  117. package/dist/oxc-runtime-emit.js +143 -0
  118. package/dist/oxc-runtime-emit.js.map +1 -0
  119. package/dist/oxc-transform.d.ts +4 -0
  120. package/dist/oxc-transform.d.ts.map +1 -0
  121. package/dist/oxc-transform.js +65 -0
  122. package/dist/oxc-transform.js.map +1 -0
  123. package/dist/oxc.d.ts +15 -0
  124. package/dist/oxc.d.ts.map +1 -0
  125. package/dist/oxc.js +250 -0
  126. package/dist/oxc.js.map +1 -0
  127. package/dist/transform.d.ts +3 -0
  128. package/dist/transform.d.ts.map +1 -0
  129. package/dist/transform.js +429 -0
  130. package/dist/transform.js.map +1 -0
  131. package/dist/types.d.ts +96 -0
  132. package/dist/types.d.ts.map +1 -0
  133. package/dist/types.js +2 -0
  134. package/dist/types.js.map +1 -0
  135. package/package.json +54 -0
@@ -0,0 +1,1103 @@
1
+ import { emitEscapeHtmlHelper } from "./emit-escape-helper.js";
2
+ import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
3
+ // Module-local handle to the URL-safety helper name for the current emit
4
+ // call. Used by deeply-nested attribute emitters to avoid threading the
5
+ // name through every signature. Reset at the top of `emitServer`.
6
+ let currentUrlSafeHelperName = "_urlAttrSafe";
7
+ let currentClientBoundaryHelperName;
8
+ // Attribute names whose value enters a navigation / script-execution
9
+ // context when the browser dereferences it. Kept in sync with
10
+ // packages/server/src/url-safety.ts.
11
+ const URL_ATTRIBUTE_NAMES = new Set([
12
+ "href",
13
+ "src",
14
+ "action",
15
+ "formaction",
16
+ "xlink:href",
17
+ "ping",
18
+ "poster",
19
+ "background",
20
+ "manifest",
21
+ ]);
22
+ function isUrlAttribute(name) {
23
+ return URL_ATTRIBUTE_NAMES.has(name);
24
+ }
25
+ // HTML-bearing attributes (Issue 077). A static string value is always
26
+ // dropped; dynamic values require the `{ __html: "..." }` opt-in.
27
+ const DANGEROUS_HTML_ATTRIBUTE_NAMES = new Set(["srcdoc"]);
28
+ function isDangerousHtmlAttribute(name) {
29
+ return DANGEROUS_HTML_ATTRIBUTE_NAMES.has(name);
30
+ }
31
+ export function emitServer(ir, options = {}) {
32
+ const escapeHelperName = allocateEscapeHelperName(ir);
33
+ const escapeBatchHelperName = options.escape === undefined
34
+ ? undefined
35
+ : allocateHelperName(ir, "_escapeHtmlBatch");
36
+ const contextProviderHelperName = usesContextProvider(ir)
37
+ ? allocateHelperName(ir, "_renderContextProviderToString")
38
+ : undefined;
39
+ const contextConsumerHelperName = usesContextConsumer(ir)
40
+ ? allocateHelperName(ir, "_renderContextConsumerToString")
41
+ : undefined;
42
+ const reactNodeRenderHelperName = usesReactNodeRender(ir)
43
+ ? allocateHelperName(ir, "_renderReactNodeToString")
44
+ : undefined;
45
+ const clientBoundaryHelperName = usesClientBoundary(ir)
46
+ ? allocateHelperName(ir, "_renderClientBoundary")
47
+ : undefined;
48
+ const outAccumulatorName = allocateHelperName(ir, "_out");
49
+ const urlSafeHelperName = allocateHelperName(ir, "_urlAttrSafe");
50
+ currentUrlSafeHelperName = urlSafeHelperName;
51
+ currentClientBoundaryHelperName = clientBoundaryHelperName;
52
+ const helper = emitEscapeHtmlHelper(escapeHelperName);
53
+ // Inline URL-scheme guard mirroring packages/server/src/url-safety.ts.
54
+ // Returns the original value when safe to emit and undefined when the
55
+ // attribute should be dropped. Inlined so compiler output stays free
56
+ // of cross-package runtime imports.
57
+ // Mirrors packages/server/src/url-safety.ts. Issue 078: in-scheme
58
+ // tab/CR/LF must be stripped anywhere in the value, not just at the
59
+ // start, to match the browser's URL parser.
60
+ const urlSafeHelper = [
61
+ `function ${urlSafeHelperName}(name, value) {`,
62
+ ` if (typeof value !== "string") return value;`,
63
+ ` const _canonical = value`,
64
+ ` .replace(/^[\\x00-\\x20]+/u, "")`,
65
+ ` .replace(/[\\t\\r\\n]/g, "");`,
66
+ ` const _match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(_canonical);`,
67
+ ` if (_match === null) return value;`,
68
+ ` const _scheme = _match[1].toLowerCase();`,
69
+ ` if (_scheme !== "javascript" && _scheme !== "vbscript" && _scheme !== "livescript" && _scheme !== "mhtml" && _scheme !== "file" && _scheme !== "data") return value;`,
70
+ ` if (_scheme === "data" && (name === "src" || name === "poster") && /^data:image\\//i.test(_canonical)) return value;`,
71
+ ` return undefined;`,
72
+ `}`,
73
+ ].join("\n");
74
+ const asyncComponentNames = collectAsyncServerComponentNames(ir.components);
75
+ const components = ir.components
76
+ .map((component) => emitComponent(component, escapeHelperName, escapeBatchHelperName, outAccumulatorName, options, asyncComponentNames, options.dynamicAttributes ?? "emit", contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName))
77
+ .join("\n\n");
78
+ // Tree-shake the URL-safety helper when it is not referenced by any
79
+ // component output. Same shape as the existing escapeImport check.
80
+ const urlSafeBlock = components.includes(urlSafeHelperName) ? urlSafeHelper : "";
81
+ const clientBoundaryBlock = clientBoundaryHelperName === undefined || !components.includes(clientBoundaryHelperName)
82
+ ? ""
83
+ : emitClientBoundaryHelper(clientBoundaryHelperName);
84
+ // Emit batch escape import only when the helper is actually referenced
85
+ // by the generated component code (issue 048: dead-import elimination).
86
+ // Helper names are uniquely allocated, so a literal substring check is
87
+ // both correct and inexpensive.
88
+ const escapeImport = options.escape === undefined ||
89
+ escapeBatchHelperName === undefined ||
90
+ !components.includes(escapeBatchHelperName)
91
+ ? ""
92
+ : `import { ${options.escape.batchImportName} as ${escapeBatchHelperName} } from ${stringLiteral(options.escape.batchImportSource)};`;
93
+ const userImports = emitUserImports(ir);
94
+ const contextImport = emitContextImport(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
95
+ const moduleStatements = emitModuleStatements(ir);
96
+ return {
97
+ code: `${[userImports, escapeImport, contextImport, moduleStatements, helper, urlSafeBlock, clientBoundaryBlock].filter(Boolean).join("\n\n")}\n\n${components}\n`,
98
+ imports: collectContextImports(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName),
99
+ };
100
+ }
101
+ function emitContextImport(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
102
+ const specifiers = [
103
+ reactNodeRenderHelperName === undefined
104
+ ? undefined
105
+ : `renderToString as ${reactNodeRenderHelperName}`,
106
+ contextProviderHelperName === undefined
107
+ ? undefined
108
+ : `renderContextProviderToString as ${contextProviderHelperName}`,
109
+ contextConsumerHelperName === undefined
110
+ ? undefined
111
+ : `renderContextConsumerToString as ${contextConsumerHelperName}`,
112
+ ].filter((specifier) => specifier !== undefined);
113
+ return specifiers.length === 0
114
+ ? ""
115
+ : `import { ${specifiers.join(", ")} } from "@reckona/mreact-compat";`;
116
+ }
117
+ function collectContextImports(contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
118
+ const specifiers = [
119
+ reactNodeRenderHelperName === undefined ? undefined : "renderToString",
120
+ contextProviderHelperName === undefined
121
+ ? undefined
122
+ : "renderContextProviderToString",
123
+ contextConsumerHelperName === undefined
124
+ ? undefined
125
+ : "renderContextConsumerToString",
126
+ ].filter((specifier) => specifier !== undefined);
127
+ return specifiers.length === 0
128
+ ? []
129
+ : [{ source: "@reckona/mreact-compat", specifiers }];
130
+ }
131
+ function emitUserImports(ir) {
132
+ return ir.userImports.join("\n");
133
+ }
134
+ function emitModuleStatements(ir) {
135
+ return ir.moduleStatements.join("\n");
136
+ }
137
+ function emitComponent(component, escapeHelperName, escapeBatchHelperName, outAccumulatorName, options, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
138
+ const body = component.bodyStatements.map((statement) => ` ${statement}`);
139
+ const parameters = component.parameters.join(", ");
140
+ const htmlStatements = collectHtmlStatements(component.root, outAccumulatorName, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
141
+ const markerStart = stringLiteral(`<!--mreact-h:start:${encodeURIComponent(component.name)}-->`);
142
+ const markerEnd = stringLiteral(`<!--mreact-h:end:${encodeURIComponent(component.name)}-->`);
143
+ const hydrationOpenStatements = options.serverHydration === true ? [` ${outAccumulatorName} += ${markerStart};`] : [];
144
+ const hydrationCloseStatements = options.serverHydration === true ? [` ${outAccumulatorName} += ${markerEnd};`] : [];
145
+ const functionKeyword = `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}${asyncComponentNames.has(component.name) ? "async " : ""}function`;
146
+ return [
147
+ `${functionKeyword} ${component.name}(${parameters}) {`,
148
+ ...body,
149
+ ` let ${outAccumulatorName} = "";`,
150
+ ...hydrationOpenStatements,
151
+ ...htmlStatements.map((statement) => ` ${statement}`),
152
+ ...hydrationCloseStatements,
153
+ ` return ${outAccumulatorName};`,
154
+ `}`,
155
+ ].join("\n");
156
+ }
157
+ function emitHtmlExpression(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
158
+ const parts = collectHtmlParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
159
+ if (parts.length === 0) {
160
+ return "\"\"";
161
+ }
162
+ return parts.join(" + ");
163
+ }
164
+ /**
165
+ * Statement-list IR walker (issue 046 followup). Produces a sequence of
166
+ * statements that each append to a shared accumulator variable instead of
167
+ * a single concat expression. Used at the top of component bodies; sub
168
+ * callbacks (`renderContextProviderToString`, async list renderers, etc.)
169
+ * still use the expression form via `emitHtmlExpression`.
170
+ *
171
+ * The benefits over expression mode:
172
+ * - intermediate string allocations from `+ +` chains disappear
173
+ * - conditional branches lower to `if/else` (no ternary expression spaghetti)
174
+ * - sync list rendering inlines the for-loop append without an IIFE wrapper
175
+ * - debugger / source maps step naturally over the generated statements
176
+ */
177
+ function collectHtmlStatements(node, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, selectedValueCode) {
178
+ if (node.kind === "text") {
179
+ const literal = escapeHtml(node.value);
180
+ if (literal === "") {
181
+ return [];
182
+ }
183
+ return [`${outVar} += ${stringLiteral(literal)};`];
184
+ }
185
+ if (node.kind === "expr") {
186
+ if (node.renderMode === "html") {
187
+ return [`${outVar} += ${rawHtmlExpression(node.code)};`];
188
+ }
189
+ if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
190
+ return [`${outVar} += ${reactNodeRenderHelperName}(() => (${node.code}));`];
191
+ }
192
+ return [`${outVar} += ${escapeHelperName}(${node.code});`];
193
+ }
194
+ if (node.kind === "conditional") {
195
+ const whenTrueStatements = node.whenTrue.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
196
+ const whenFalseStatements = node.whenFalse.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
197
+ if (whenTrueStatements.length === 0 && whenFalseStatements.length === 0) {
198
+ return [];
199
+ }
200
+ if (whenFalseStatements.length === 0) {
201
+ return [
202
+ `if (${node.conditionCode}) {`,
203
+ ...whenTrueStatements.map((statement) => ` ${statement}`),
204
+ `}`,
205
+ ];
206
+ }
207
+ if (whenTrueStatements.length === 0) {
208
+ return [
209
+ `if (!(${node.conditionCode})) {`,
210
+ ...whenFalseStatements.map((statement) => ` ${statement}`),
211
+ `}`,
212
+ ];
213
+ }
214
+ return [
215
+ `if (${node.conditionCode}) {`,
216
+ ...whenTrueStatements.map((statement) => ` ${statement}`),
217
+ `} else {`,
218
+ ...whenFalseStatements.map((statement) => ` ${statement}`),
219
+ `}`,
220
+ ];
221
+ }
222
+ if (node.kind === "list") {
223
+ const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
224
+ if (isAsync) {
225
+ // Parallel async path keeps the existing renderer + Promise.all + join
226
+ // form to preserve concurrent resolution semantics.
227
+ const parameters = node.indexName === undefined
228
+ ? node.itemName
229
+ : `${node.itemName}, ${node.indexName}`;
230
+ const renderer = emitListRenderer(node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
231
+ const mapped = `(${node.itemsCode}).map(${renderer})`;
232
+ return [`${outVar} += (await Promise.all(${mapped})).join("");`];
233
+ }
234
+ // Sync list — inline for-loop appending to the caller's accumulator.
235
+ // No inner IIFE wrapper and no intermediate string concat per iteration.
236
+ const itemBinding = `const ${node.itemName} = _arr[_i];`;
237
+ const indexBinding = node.indexName === undefined ? undefined : `const ${node.indexName} = _i;`;
238
+ const bodyStatements = node.bodyStatements ?? [];
239
+ const childStatements = node.children.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
240
+ return [
241
+ `{`,
242
+ ` const _arr = (${node.itemsCode});`,
243
+ ` for (let _i = 0, _len = _arr.length; _i < _len; _i++) {`,
244
+ ` ${itemBinding}`,
245
+ ...(indexBinding === undefined ? [] : [` ${indexBinding}`]),
246
+ ...bodyStatements.map((statement) => ` ${statement}`),
247
+ ...childStatements.map((statement) => ` ${statement}`),
248
+ ` }`,
249
+ `}`,
250
+ ];
251
+ }
252
+ if (node.kind === "fragment") {
253
+ return node.children.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
254
+ }
255
+ if (node.kind === "component") {
256
+ if (node.name === "Suspense") {
257
+ return [
258
+ `${outVar} += "<!--$-->";`,
259
+ ...node.children.flatMap((child) => collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)),
260
+ `${outVar} += "<!--/$-->";`,
261
+ ];
262
+ }
263
+ if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
264
+ // Provider helper takes a string-returning callback. Use the
265
+ // expression form inside the callback to preserve the existing
266
+ // helper contract.
267
+ const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
268
+ return [
269
+ `${outVar} += ${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
270
+ ];
271
+ }
272
+ if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
273
+ const renderProp = findComponentRenderProp(node.props, "children");
274
+ if (renderProp !== undefined) {
275
+ const valueName = renderProp.valueName ?? "_value";
276
+ return [
277
+ `${outVar} += ${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
278
+ ];
279
+ }
280
+ }
281
+ if (isClientBoundaryPlaceholder(node)) {
282
+ const helperName = currentClientBoundaryHelperName;
283
+ if (helperName !== undefined) {
284
+ return [
285
+ `${outVar} += ${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
286
+ ];
287
+ }
288
+ return [`${outVar} += ${stringLiteral(clientBoundaryPlaceholder(node))};`];
289
+ }
290
+ if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
291
+ return [
292
+ `${outVar} += ${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)});`,
293
+ ];
294
+ }
295
+ return [
296
+ `${outVar} += ${emitComponentCallExpression(node.name, emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName), asyncComponentNames)};`,
297
+ ];
298
+ }
299
+ if (node.kind === "async-boundary") {
300
+ return [];
301
+ }
302
+ // element
303
+ const statements = [];
304
+ if (node.tagName === "textarea") {
305
+ statements.push(`${outVar} += ${stringLiteral("<textarea")};`);
306
+ for (const attributePart of collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes)) {
307
+ statements.push(`${outVar} += ${attributePart};`);
308
+ }
309
+ statements.push(`${outVar} += ">";`);
310
+ for (const valuePart of collectTextareaValueParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)) {
311
+ statements.push(`${outVar} += ${valuePart};`);
312
+ }
313
+ statements.push(`${outVar} += "</textarea>";`);
314
+ return statements;
315
+ }
316
+ statements.push(`${outVar} += ${stringLiteral(`<${node.tagName}`)};`);
317
+ for (const attributePart of collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes)) {
318
+ statements.push(`${outVar} += ${attributePart};`);
319
+ }
320
+ const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
321
+ if (selectedAttributePart !== undefined) {
322
+ statements.push(`${outVar} += ${selectedAttributePart};`);
323
+ }
324
+ statements.push(`${outVar} += ">";`);
325
+ const childrenExpression = emitBatchedSimpleChildrenExpression(node.children, escapeBatchHelperName);
326
+ const childSelectedValueCode = node.tagName === "select"
327
+ ? findFormValueAttributeCode(node.attributes)
328
+ : undefined;
329
+ if (childrenExpression !== undefined && childSelectedValueCode === undefined) {
330
+ statements.push(`${outVar} += ${childrenExpression};`);
331
+ }
332
+ else {
333
+ for (const child of node.children) {
334
+ statements.push(...collectHtmlStatements(child, outVar, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, childSelectedValueCode));
335
+ }
336
+ }
337
+ statements.push(`${outVar} += ${stringLiteral(`</${node.tagName}>`)};`);
338
+ return statements;
339
+ }
340
+ function collectHtmlParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, selectedValueCode) {
341
+ if (node.kind === "text") {
342
+ return [stringLiteral(escapeHtml(node.value))];
343
+ }
344
+ if (node.kind === "expr") {
345
+ if (node.renderMode === "html") {
346
+ return [rawHtmlExpression(node.code)];
347
+ }
348
+ if (node.renderMode === "react-node" && reactNodeRenderHelperName !== undefined) {
349
+ return [`${reactNodeRenderHelperName}(() => (${node.code}))`];
350
+ }
351
+ return [`${escapeHelperName}(${node.code})`];
352
+ }
353
+ if (node.kind === "conditional") {
354
+ return [
355
+ `((${node.conditionCode}) ? ${emitHtmlExpressionFromChildren(node.whenTrue, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)} : ${emitHtmlExpressionFromChildren(node.whenFalse, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
356
+ ];
357
+ }
358
+ if (node.kind === "list") {
359
+ const isAsync = containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
360
+ if (isAsync) {
361
+ // Async lists rely on Promise.all() for parallel resolution; the
362
+ // callback allocation is amortized across `await` latency, so we keep
363
+ // the `.map().then(...).join("")` form.
364
+ const parameters = node.indexName === undefined
365
+ ? node.itemName
366
+ : `${node.itemName}, ${node.indexName}`;
367
+ const renderer = emitListRenderer(node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
368
+ const mapped = `(${node.itemsCode}).map(${renderer})`;
369
+ return [`(await Promise.all(${mapped})).join("")`];
370
+ }
371
+ // Synchronous list — imperative accumulator avoids the per-render
372
+ // callback allocation, the intermediate `.map()` result array, and the
373
+ // trailing `.join("")` call.
374
+ return [emitSyncListIife(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)];
375
+ }
376
+ if (node.kind === "fragment") {
377
+ return node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
378
+ }
379
+ if (node.kind === "component") {
380
+ if (node.name === "Suspense") {
381
+ return [
382
+ stringLiteral("<!--$-->"),
383
+ ...node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)),
384
+ stringLiteral("<!--/$-->"),
385
+ ];
386
+ }
387
+ if (contextProviderHelperName !== undefined && node.name.endsWith(".Provider")) {
388
+ const valueCode = findComponentPropCode(node.props, "value") ?? "undefined";
389
+ return [
390
+ `${contextProviderHelperName}(${node.name}, ${valueCode}, () => ${emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
391
+ ];
392
+ }
393
+ if (contextConsumerHelperName !== undefined && node.name.endsWith(".Consumer")) {
394
+ const renderProp = findComponentRenderProp(node.props, "children");
395
+ if (renderProp !== undefined) {
396
+ const valueName = renderProp.valueName ?? "_value";
397
+ return [
398
+ `${contextConsumerHelperName}(${node.name}, (${valueName}) => ${emitHtmlExpressionFromChildren(renderProp.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
399
+ ];
400
+ }
401
+ }
402
+ if (isClientBoundaryPlaceholder(node)) {
403
+ const helperName = currentClientBoundaryHelperName;
404
+ if (helperName !== undefined) {
405
+ return [
406
+ `${helperName}(${stringLiteral(node.name)}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
407
+ ];
408
+ }
409
+ return [stringLiteral(clientBoundaryPlaceholder(node))];
410
+ }
411
+ if (node.runtime === "compat" && reactNodeRenderHelperName !== undefined) {
412
+ return [
413
+ `${reactNodeRenderHelperName}(${node.name}, ${emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)})`,
414
+ ];
415
+ }
416
+ return [
417
+ emitComponentCallExpression(node.name, emitPropsObject(node.props, node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName), asyncComponentNames),
418
+ ];
419
+ }
420
+ if (node.kind === "async-boundary") {
421
+ return [];
422
+ }
423
+ const closeTag = `</${node.tagName}>`;
424
+ if (node.tagName === "textarea") {
425
+ return [
426
+ stringLiteral("<textarea"),
427
+ ...collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes),
428
+ stringLiteral(">"),
429
+ ...collectTextareaValueParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName),
430
+ stringLiteral(closeTag),
431
+ ];
432
+ }
433
+ const childrenExpression = emitBatchedSimpleChildrenExpression(node.children, escapeBatchHelperName);
434
+ const childSelectedValueCode = node.tagName === "select"
435
+ ? findFormValueAttributeCode(node.attributes)
436
+ : undefined;
437
+ const selectedAttributePart = collectOptionSelectedAttributePart(node, selectedValueCode);
438
+ return [
439
+ stringLiteral(`<${node.tagName}`),
440
+ ...collectElementAttributeParts(node.tagName, node.attributes, escapeHelperName, escapeBatchHelperName, dynamicAttributes),
441
+ ...(selectedAttributePart === undefined ? [] : [selectedAttributePart]),
442
+ stringLiteral(">"),
443
+ ...(childrenExpression === undefined || childSelectedValueCode !== undefined
444
+ ? node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName, childSelectedValueCode))
445
+ : [childrenExpression]),
446
+ stringLiteral(closeTag),
447
+ ];
448
+ }
449
+ function collectHtmlAttributeParts(tagName, attr, escapeHelperName, escapeBatchHelperName, dynamicAttributes) {
450
+ if (attr.kind === "event" || attr.kind === "spread-attr" || attr.name === "key") {
451
+ return [];
452
+ }
453
+ const htmlName = htmlAttributeNameForElement(tagName, attr.name);
454
+ if (attr.kind === "static-attr") {
455
+ // Reject literal `javascript:` / `data:` / etc. in JSX source. This
456
+ // never produces a runtime branch because the value is known at
457
+ // compile time -- we just drop the attribute (matching the dynamic
458
+ // path) so a developer cannot statically introduce the same XSS.
459
+ if (isUrlAttribute(htmlName) && isStaticUrlValueUnsafe(htmlName, attr.value)) {
460
+ return [];
461
+ }
462
+ // Issue 077: a literal string value for `srcdoc` etc. can never be
463
+ // the `{ __html: ... }` opt-in shape, so it is dropped at compile
464
+ // time.
465
+ if (isDangerousHtmlAttribute(htmlName)) {
466
+ return [];
467
+ }
468
+ return [`${stringLiteral(` ${htmlName}="${escapeHtml(attr.value)}"`)}`];
469
+ }
470
+ if (dynamicAttributes === "drop") {
471
+ return [];
472
+ }
473
+ if (attr.name === "style") {
474
+ return [emitDynamicStyleAttributeExpression(attr.code, escapeHelperName, escapeBatchHelperName)];
475
+ }
476
+ if (isDangerousHtmlAttribute(htmlName)) {
477
+ // Dynamic srcdoc must arrive as `{ __html: "..." }`. Drop anything
478
+ // else at runtime so a value computed from a loader cannot inject
479
+ // executable HTML into the iframe document.
480
+ return [
481
+ `(() => { 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 ""; })()`,
482
+ ];
483
+ }
484
+ return [emitDynamicAttributeExpression(htmlName, attr.code, escapeHelperName)];
485
+ }
486
+ function isStaticUrlValueUnsafe(name, value) {
487
+ // Same canonicalization as the runtime helper (Issue 078).
488
+ let start = 0;
489
+ while (start < value.length && value.charCodeAt(start) <= 0x20) {
490
+ start += 1;
491
+ }
492
+ const canonical = value.slice(start).replace(/[\t\r\n]/g, "");
493
+ const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(canonical);
494
+ if (match === null || match[1] === undefined)
495
+ return false;
496
+ const scheme = match[1].toLowerCase();
497
+ if (scheme === "javascript" || scheme === "vbscript" || scheme === "livescript" || scheme === "mhtml" || scheme === "file")
498
+ return true;
499
+ if (scheme === "data") {
500
+ if ((name === "src" || name === "poster") && /^data:image\//i.test(canonical))
501
+ return false;
502
+ return true;
503
+ }
504
+ return false;
505
+ }
506
+ function collectElementAttributeParts(tagName, attrs, escapeHelperName, escapeBatchHelperName, dynamicAttributes) {
507
+ const hasExplicitInputValue = tagName === "input" &&
508
+ attrs.some((attr) => attr.kind !== "spread-attr" && attr.name === "value");
509
+ const hasExplicitInputChecked = tagName === "input" &&
510
+ attrs.some((attr) => attr.kind !== "spread-attr" && attr.name === "checked");
511
+ return attrs.flatMap((attr) => attr.kind !== "spread-attr" &&
512
+ ((tagName === "input" &&
513
+ ((attr.name === "defaultValue" && hasExplicitInputValue) ||
514
+ (attr.name === "defaultChecked" && hasExplicitInputChecked))) ||
515
+ ((tagName === "textarea" || tagName === "select") &&
516
+ (attr.name === "value" || attr.name === "defaultValue")))
517
+ ? []
518
+ : collectHtmlAttributeParts(tagName, attr, escapeHelperName, escapeBatchHelperName, dynamicAttributes));
519
+ }
520
+ function emitDynamicAttributeExpression(name, code, escapeHelperName) {
521
+ if (isUrlAttribute(name)) {
522
+ // Run the value through the inline URL safety helper. The helper
523
+ // returns the value when safe and `undefined` when the attribute
524
+ // should be dropped. Using an IIFE here is necessary because we
525
+ // need to capture the value once and branch on the helper output.
526
+ 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("\"")}; })()`;
527
+ }
528
+ const inlineExpr = simpleSideEffectFreeExpression(code);
529
+ if (inlineExpr !== undefined) {
530
+ // Inline 3 evaluations to avoid per-attribute IIFE closure allocation.
531
+ // Safe because `simpleSideEffectFreeExpression` only matches expressions
532
+ // whose evaluation has no observable side effects (identifier read,
533
+ // member chain, literal, this).
534
+ return `(${inlineExpr} == null || ${inlineExpr} === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(${inlineExpr} === true ? "" : ${inlineExpr}) + ${stringLiteral("\"")})`;
535
+ }
536
+ return `(() => { const _value = (${code}); return _value == null || _value === false ? "" : ${stringLiteral(` ${name}="`)} + ${escapeHelperName}(_value === true ? "" : _value) + ${stringLiteral("\"")}; })()`;
537
+ }
538
+ function emitDynamicStyleAttributeExpression(code, escapeHelperName, escapeBatchHelperName) {
539
+ const staticStyleExpression = emitStaticStyleObjectAttributeExpression(code, escapeHelperName);
540
+ if (staticStyleExpression !== undefined) {
541
+ return staticStyleExpression;
542
+ }
543
+ const escapedPair = escapeBatchHelperName === undefined
544
+ ? `${escapeHelperName}(_cssName) + ":" + ${escapeHelperName}(_styleValue === true ? "" : _styleValue)`
545
+ : `(() => { const _escaped = ${escapeBatchHelperName}([_cssName, _styleValue === true ? "" : _styleValue]); return _escaped[0] + ":" + _escaped[1]; })()`;
546
+ 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("\"")}; })()`;
547
+ }
548
+ function emitStaticStyleObjectAttributeExpression(code, escapeHelperName) {
549
+ const entries = parseStaticStyleObjectLiteral(code);
550
+ if (entries === undefined) {
551
+ return undefined;
552
+ }
553
+ if (entries.length === 0) {
554
+ return `""`;
555
+ }
556
+ // Stage B — all values are compile-time literals: collapse to a single
557
+ // constant string. null/false entries are dropped at build time.
558
+ const literalEntries = entries.map((entry) => ({
559
+ cssName: entry.cssName,
560
+ literal: parseStyleLiteralValue(entry.valueCode),
561
+ }));
562
+ if (literalEntries.every((entry) => entry.literal !== undefined)) {
563
+ const parts = literalEntries
564
+ .filter((entry) => entry.literal !== null)
565
+ .map((entry) => `${entry.cssName}:${escapeHtml(String(entry.literal))}`);
566
+ if (parts.length === 0) {
567
+ return `""`;
568
+ }
569
+ return stringLiteral(` style="${parts.join(";")}"`);
570
+ }
571
+ // Stage A — needSep tracking with inline string accumulator, no intermediate
572
+ // array allocation and no `.join(";")` per render.
573
+ const statements = entries.map((entry) => `{ const _v = (${entry.valueCode}); if (_v != null && _v !== false) _style += (_style === "" ? "" : ";") + ${stringLiteral(`${entry.cssName}:`)} + ${escapeHelperName}(_v === true ? "" : _v); }`);
574
+ return `(() => { let _style = ""; ${statements.join(" ")} return _style === "" ? "" : ${stringLiteral(" style=\"")} + _style + ${stringLiteral("\"")}; })()`;
575
+ }
576
+ /**
577
+ * Returns the value (as JS value) if `code` is a build-time literal whose
578
+ * stringification is deterministic and safe to embed in style serialization.
579
+ * Returns `null` for compile-time `null`/`false`/`undefined` (i.e., entries
580
+ * that should be dropped). Returns `undefined` if the value isn't a literal.
581
+ */
582
+ function parseStyleLiteralValue(code) {
583
+ const trimmed = unwrapParenthesized(code.trim());
584
+ if (trimmed === "null" || trimmed === "false" || trimmed === "undefined") {
585
+ return null;
586
+ }
587
+ if (trimmed === "true") {
588
+ return "";
589
+ }
590
+ if (NUMERIC_LITERAL_RE.test(trimmed)) {
591
+ return Number(trimmed);
592
+ }
593
+ if (SIMPLE_STRING_LITERAL_RE.test(trimmed)) {
594
+ return JSON.parse(trimmed);
595
+ }
596
+ if (SIMPLE_SINGLE_QUOTE_RE.test(trimmed)) {
597
+ return JSON.parse(`"${trimmed.slice(1, -1).replaceAll('"', '\\"')}"`);
598
+ }
599
+ return undefined;
600
+ }
601
+ function parseStaticStyleObjectLiteral(code) {
602
+ const objectCode = unwrapParenthesized(code.trim());
603
+ if (!objectCode.startsWith("{") || !objectCode.endsWith("}")) {
604
+ return undefined;
605
+ }
606
+ const body = objectCode.slice(1, -1).trim();
607
+ if (body === "") {
608
+ return [];
609
+ }
610
+ const entries = [];
611
+ for (const property of splitTopLevel(body, ",")) {
612
+ const trimmed = property.trim();
613
+ if (trimmed === "" || trimmed.startsWith("...") || trimmed.startsWith("[")) {
614
+ return undefined;
615
+ }
616
+ const colonIndex = findTopLevelColon(trimmed);
617
+ if (colonIndex < 0) {
618
+ return undefined;
619
+ }
620
+ const rawKey = trimmed.slice(0, colonIndex).trim();
621
+ const valueCode = trimmed.slice(colonIndex + 1).trim();
622
+ const key = parseStaticObjectKey(rawKey);
623
+ if (key === undefined || valueCode === "") {
624
+ return undefined;
625
+ }
626
+ entries.push({ cssName: cssPropertyName(key), valueCode });
627
+ }
628
+ return entries;
629
+ }
630
+ // Matches an identifier or member-access chain such as `foo`, `foo.bar`, or
631
+ // `this.cell.row`. Computed access (`foo[i]`) is excluded because the index
632
+ // can itself have side effects.
633
+ const SIMPLE_IDENT_CHAIN_RE = /^(this|[A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*$/;
634
+ const NUMERIC_LITERAL_RE = /^-?(?:\d+(?:\.\d+)?|\.\d+)$/;
635
+ const SIMPLE_STRING_LITERAL_RE = /^"(?:[^"\\]|\\.)*"$/;
636
+ const SIMPLE_SINGLE_QUOTE_RE = /^'(?:[^'\\]|\\.)*'$/;
637
+ /**
638
+ * Returns the normalized source if `code` is a side-effect-free expression
639
+ * safe to evaluate multiple times inline, otherwise undefined.
640
+ *
641
+ * Used by attribute emit to skip the per-attribute IIFE closure allocation
642
+ * when the value can be re-evaluated cheaply (Identifier / MemberExpression
643
+ * chain / literal / `this`).
644
+ */
645
+ function simpleSideEffectFreeExpression(code) {
646
+ const trimmed = unwrapParenthesized(code.trim());
647
+ if (trimmed === "") {
648
+ return undefined;
649
+ }
650
+ if (trimmed === "true" ||
651
+ trimmed === "false" ||
652
+ trimmed === "null" ||
653
+ trimmed === "undefined") {
654
+ return trimmed;
655
+ }
656
+ if (NUMERIC_LITERAL_RE.test(trimmed) ||
657
+ SIMPLE_STRING_LITERAL_RE.test(trimmed) ||
658
+ SIMPLE_SINGLE_QUOTE_RE.test(trimmed) ||
659
+ SIMPLE_IDENT_CHAIN_RE.test(trimmed)) {
660
+ return trimmed;
661
+ }
662
+ return undefined;
663
+ }
664
+ function unwrapParenthesized(code) {
665
+ let current = code;
666
+ while (current.startsWith("(") && current.endsWith(")") && findMatchingClose(current, 0) === current.length - 1) {
667
+ current = current.slice(1, -1).trim();
668
+ }
669
+ return current;
670
+ }
671
+ function splitTopLevel(code, separator) {
672
+ const parts = [];
673
+ let start = 0;
674
+ let depth = 0;
675
+ let quote;
676
+ for (let index = 0; index < code.length; index += 1) {
677
+ const char = code[index];
678
+ if (quote !== undefined) {
679
+ if (char === "\\") {
680
+ index += 1;
681
+ }
682
+ else if (char === quote) {
683
+ quote = undefined;
684
+ }
685
+ continue;
686
+ }
687
+ if (char === "\"" || char === "'" || char === "`") {
688
+ quote = char;
689
+ continue;
690
+ }
691
+ if (char === "{" || char === "[" || char === "(") {
692
+ depth += 1;
693
+ continue;
694
+ }
695
+ if (char === "}" || char === "]" || char === ")") {
696
+ depth -= 1;
697
+ continue;
698
+ }
699
+ if (depth === 0 && char === separator) {
700
+ parts.push(code.slice(start, index));
701
+ start = index + 1;
702
+ }
703
+ }
704
+ parts.push(code.slice(start));
705
+ return parts;
706
+ }
707
+ function findTopLevelColon(code) {
708
+ return splitTopLevel(code, ":")[0]?.length ?? -1;
709
+ }
710
+ function findMatchingClose(code, openIndex) {
711
+ let depth = 0;
712
+ let quote;
713
+ for (let index = openIndex; index < code.length; index += 1) {
714
+ const char = code[index];
715
+ if (quote !== undefined) {
716
+ if (char === "\\") {
717
+ index += 1;
718
+ }
719
+ else if (char === quote) {
720
+ quote = undefined;
721
+ }
722
+ continue;
723
+ }
724
+ if (char === "\"" || char === "'" || char === "`") {
725
+ quote = char;
726
+ continue;
727
+ }
728
+ if (char === "(") {
729
+ depth += 1;
730
+ }
731
+ else if (char === ")") {
732
+ depth -= 1;
733
+ if (depth === 0) {
734
+ return index;
735
+ }
736
+ }
737
+ }
738
+ return -1;
739
+ }
740
+ function parseStaticObjectKey(rawKey) {
741
+ if (/^[A-Za-z_$][\w$-]*$/.test(rawKey)) {
742
+ return rawKey;
743
+ }
744
+ if ((rawKey.startsWith("\"") && rawKey.endsWith("\"")) ||
745
+ (rawKey.startsWith("'") && rawKey.endsWith("'"))) {
746
+ return rawKey.slice(1, -1);
747
+ }
748
+ return undefined;
749
+ }
750
+ function cssPropertyName(name) {
751
+ return name.startsWith("--")
752
+ ? name
753
+ : name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
754
+ }
755
+ function htmlAttributeName(name) {
756
+ return HTML_ATTRIBUTE_ALIASES[name] ?? name;
757
+ }
758
+ const HTML_ATTRIBUTE_ALIASES = {
759
+ acceptCharset: "accept-charset",
760
+ autoFocus: "autofocus",
761
+ autoPlay: "autoplay",
762
+ charSet: "charset",
763
+ className: "class",
764
+ colSpan: "colspan",
765
+ contentEditable: "contenteditable",
766
+ crossOrigin: "crossorigin",
767
+ encType: "enctype",
768
+ formAction: "formaction",
769
+ frameBorder: "frameborder",
770
+ htmlFor: "for",
771
+ httpEquiv: "http-equiv",
772
+ maxLength: "maxlength",
773
+ minLength: "minlength",
774
+ noValidate: "novalidate",
775
+ playsInline: "playsinline",
776
+ readOnly: "readonly",
777
+ rowSpan: "rowspan",
778
+ spellCheck: "spellcheck",
779
+ srcDoc: "srcdoc",
780
+ srcSet: "srcset",
781
+ tabIndex: "tabindex",
782
+ useMap: "usemap",
783
+ };
784
+ function findFormValueAttributeCode(attrs) {
785
+ const valueAttr = attrs.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
786
+ const defaultValueAttr = attrs.find((attr) => attr.kind !== "spread-attr" && attr.name === "defaultValue");
787
+ const attr = valueAttr ?? defaultValueAttr;
788
+ if (attr === undefined || attr.kind === "event" || attr.kind === "spread-attr") {
789
+ return undefined;
790
+ }
791
+ return attr.kind === "static-attr" ? stringLiteral(attr.value) : `(${attr.code})`;
792
+ }
793
+ function collectTextareaValueParts(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
794
+ const valueCode = findFormValueAttributeCode(node.attributes);
795
+ if (valueCode !== undefined) {
796
+ return [`${escapeHelperName}(${valueCode})`];
797
+ }
798
+ return node.children.flatMap((child) => collectHtmlParts(child, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName));
799
+ }
800
+ function collectOptionSelectedAttributePart(node, selectedValueCode) {
801
+ if (selectedValueCode === undefined || node.tagName !== "option") {
802
+ return undefined;
803
+ }
804
+ const optionValueCode = findOptionValueCode(node);
805
+ if (optionValueCode === undefined) {
806
+ return undefined;
807
+ }
808
+ return `(() => { const _selected = (${selectedValueCode}); return _selected == null ? "" : String(_selected) === String(${optionValueCode}) ? ${stringLiteral(' selected=""')} : ""; })()`;
809
+ }
810
+ function findOptionValueCode(node) {
811
+ const valueAttr = node.attributes.find((attr) => attr.kind !== "spread-attr" && attr.name === "value");
812
+ if (valueAttr !== undefined && valueAttr.kind !== "event" && valueAttr.kind !== "spread-attr") {
813
+ return valueAttr.kind === "static-attr" ? stringLiteral(valueAttr.value) : `(${valueAttr.code})`;
814
+ }
815
+ return node.children.every((child) => child.kind === "text")
816
+ ? stringLiteral(node.children.map((child) => child.value).join(""))
817
+ : undefined;
818
+ }
819
+ function htmlAttributeNameForElement(tagName, name) {
820
+ if (tagName === "input") {
821
+ if (name === "defaultValue") {
822
+ return "value";
823
+ }
824
+ if (name === "defaultChecked") {
825
+ return "checked";
826
+ }
827
+ }
828
+ return htmlAttributeName(name);
829
+ }
830
+ function rawHtmlExpression(code) {
831
+ return `(() => { const _value = (${code}); return Array.isArray(_value) ? _value.join("") : String(_value ?? ""); })()`;
832
+ }
833
+ function emitBatchedSimpleChildrenExpression(children, escapeBatchHelperName) {
834
+ if (escapeBatchHelperName === undefined) {
835
+ return undefined;
836
+ }
837
+ const dynamicChildren = children.filter((child) => child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node");
838
+ if (dynamicChildren.length < 2) {
839
+ return undefined;
840
+ }
841
+ if (children.some((child) => child.kind !== "text" &&
842
+ !(child.kind === "expr" && child.renderMode !== "html" && child.renderMode !== "react-node"))) {
843
+ return undefined;
844
+ }
845
+ const values = dynamicChildren.map((child) => child.code);
846
+ let dynamicIndex = 0;
847
+ const pieces = children.map((child) => {
848
+ if (child.kind === "text") {
849
+ return stringLiteral(escapeHtml(child.value));
850
+ }
851
+ const index = dynamicIndex;
852
+ dynamicIndex += 1;
853
+ return `_escaped[${index}]`;
854
+ });
855
+ return `(() => { const _escaped = ${escapeBatchHelperName}([${values.join(", ")}]); return ${pieces.join(" + ")}; })()`;
856
+ }
857
+ function emitHtmlExpressionFromChildren(children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
858
+ if (children.length === 0) {
859
+ return "\"\"";
860
+ }
861
+ return emitHtmlExpression({ kind: "fragment", children }, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
862
+ }
863
+ function emitSyncListIife(node, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
864
+ const valueExpression = emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
865
+ const itemBinding = `const ${node.itemName} = _arr[_i];`;
866
+ const indexBinding = node.indexName === undefined ? "" : ` const ${node.indexName} = _i;`;
867
+ const bodyStatements = node.bodyStatements === undefined || node.bodyStatements.length === 0
868
+ ? ""
869
+ : ` ${node.bodyStatements.join(" ")}`;
870
+ return `(() => { let _o = ""; const _arr = (${node.itemsCode}); for (let _i = 0, _len = _arr.length; _i < _len; _i++) { ${itemBinding}${indexBinding}${bodyStatements} _o += ${valueExpression}; } return _o; })()`;
871
+ }
872
+ function emitListRenderer(node, parameters, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
873
+ const valueExpression = emitHtmlExpressionFromChildren(node.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName);
874
+ const asyncKeyword = containsAsyncServerOperationInChildren(node.children, asyncComponentNames)
875
+ ? "async "
876
+ : "";
877
+ if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
878
+ return `${asyncKeyword}(${parameters}) => ${valueExpression}`;
879
+ }
880
+ return `${asyncKeyword}(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
881
+ }
882
+ function emitPropsObject(props, children = [], escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName) {
883
+ const entries = props.map((prop) => {
884
+ if (prop.kind === "spread-prop") {
885
+ return `...(${prop.code})`;
886
+ }
887
+ if (prop.kind === "render-prop") {
888
+ return `${emitPropName(prop.name)}: ${emitHtmlExpressionFromChildren(prop.children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`;
889
+ }
890
+ return `${emitPropName(prop.name)}: (${prop.code})`;
891
+ });
892
+ if (children.length > 0) {
893
+ entries.push(`children: ${emitHtmlExpressionFromChildren(children, escapeHelperName, escapeBatchHelperName, asyncComponentNames, dynamicAttributes, contextProviderHelperName, contextConsumerHelperName, reactNodeRenderHelperName)}`);
894
+ }
895
+ return `{ ${entries.join(", ")} }`;
896
+ }
897
+ function emitComponentCallExpression(name, propsCode, asyncComponentNames) {
898
+ const call = `${name}(${propsCode})`;
899
+ return asyncComponentNames.has(name) ? `(await ${call})` : call;
900
+ }
901
+ function isClientBoundaryPlaceholder(node) {
902
+ return node.clientReference !== undefined && !isCompatClientReference(node);
903
+ }
904
+ function isCompatClientReference(node) {
905
+ return node.clientReference !== undefined && /\.(?:compat)\.[cm]?[jt]sx?$/.test(node.clientReference.moduleId);
906
+ }
907
+ function clientBoundaryPlaceholder(node) {
908
+ return `<!--mreact-client-boundary:${escapeHtml(node.name)}-->`;
909
+ }
910
+ function collectAsyncServerComponentNames(components) {
911
+ const names = new Set(components
912
+ .filter((component) => component.async === true)
913
+ .map((component) => component.name));
914
+ let changed = true;
915
+ while (changed) {
916
+ changed = false;
917
+ for (const component of components) {
918
+ if (!names.has(component.name) &&
919
+ containsAsyncServerOperation(component.root, names)) {
920
+ names.add(component.name);
921
+ changed = true;
922
+ }
923
+ }
924
+ }
925
+ return names;
926
+ }
927
+ function containsAsyncServerOperationInChildren(children, asyncComponentNames) {
928
+ return children.some((child) => containsAsyncServerOperation(child, asyncComponentNames));
929
+ }
930
+ function containsAsyncServerOperation(node, asyncComponentNames) {
931
+ if (node.kind === "async-boundary") {
932
+ return true;
933
+ }
934
+ if (node.kind === "component") {
935
+ if (node.runtime === "compat" && !isClientBoundaryPlaceholder(node)) {
936
+ return true;
937
+ }
938
+ return (asyncComponentNames.has(node.name) ||
939
+ containsAsyncServerOperationInChildren(node.children, asyncComponentNames) ||
940
+ node.props.some((prop) => prop.kind === "render-prop" &&
941
+ containsAsyncServerOperationInChildren(prop.children, asyncComponentNames)));
942
+ }
943
+ if (node.kind === "conditional") {
944
+ return containsAsyncServerOperationInChildren([...node.whenTrue, ...node.whenFalse], asyncComponentNames);
945
+ }
946
+ if (node.kind === "list") {
947
+ return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
948
+ }
949
+ if (node.kind === "element" || node.kind === "fragment") {
950
+ return containsAsyncServerOperationInChildren(node.children, asyncComponentNames);
951
+ }
952
+ return false;
953
+ }
954
+ function emitPropName(name) {
955
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
956
+ }
957
+ function allocateEscapeHelperName(ir) {
958
+ return allocateHelperName(ir, "_escapeHtml");
959
+ }
960
+ function allocateHelperName(ir, baseName) {
961
+ const reservedNames = new Set(ir.moduleBindingNames);
962
+ for (const component of ir.components) {
963
+ reservedNames.add(component.name);
964
+ for (const bindingName of component.bindingNames) {
965
+ reservedNames.add(bindingName);
966
+ }
967
+ }
968
+ let name = baseName;
969
+ let index = 1;
970
+ while (reservedNames.has(name)) {
971
+ name = `${baseName}$${index}`;
972
+ index += 1;
973
+ }
974
+ return name;
975
+ }
976
+ function usesContextProvider(ir) {
977
+ return ir.components.some((component) => containsContextProvider(component.root));
978
+ }
979
+ function usesContextConsumer(ir) {
980
+ return ir.components.some((component) => containsContextConsumer(component.root));
981
+ }
982
+ function usesReactNodeRender(ir) {
983
+ return ir.components.some((component) => containsReactNodeRender(component.root));
984
+ }
985
+ function usesClientBoundary(ir) {
986
+ return ir.components.some((component) => containsClientBoundary(component.root));
987
+ }
988
+ function emitClientBoundaryHelper(name) {
989
+ return [
990
+ `function ${name}(name, props) {`,
991
+ ` const _name = String(name);`,
992
+ ` const _escapedName = _name.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");`,
993
+ ` const _json = (JSON.stringify(props ?? {}) ?? "{}").replaceAll("<", "\\\\u003c");`,
994
+ ` return \`<template data-mreact-client-boundary="\${_escapedName}"></template><script type="application/json" data-mreact-client-boundary-props="\${_escapedName}">\${_json}</script>\`;`,
995
+ `}`,
996
+ ].join("\n");
997
+ }
998
+ function containsClientBoundary(node) {
999
+ if (node.kind === "component" && isClientBoundaryPlaceholder(node)) {
1000
+ return true;
1001
+ }
1002
+ if (node.kind === "conditional") {
1003
+ return [...node.whenTrue, ...node.whenFalse].some(containsClientBoundary);
1004
+ }
1005
+ if (node.kind === "list") {
1006
+ return node.children.some(containsClientBoundary);
1007
+ }
1008
+ if (node.kind === "fragment" || node.kind === "element") {
1009
+ return node.children.some(containsClientBoundary);
1010
+ }
1011
+ if (node.kind === "component") {
1012
+ return (node.children.some(containsClientBoundary) ||
1013
+ node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsClientBoundary)));
1014
+ }
1015
+ if (node.kind === "async-boundary") {
1016
+ return (node.children.some(containsClientBoundary) ||
1017
+ node.placeholderChildren?.some(containsClientBoundary) === true ||
1018
+ node.catchChildren?.some(containsClientBoundary) === true);
1019
+ }
1020
+ return false;
1021
+ }
1022
+ function containsReactNodeRender(node) {
1023
+ if (node.kind === "expr") {
1024
+ return node.renderMode === "react-node";
1025
+ }
1026
+ if (node.kind === "conditional") {
1027
+ return [...node.whenTrue, ...node.whenFalse].some(containsReactNodeRender);
1028
+ }
1029
+ if (node.kind === "list") {
1030
+ return node.children.some(containsReactNodeRender);
1031
+ }
1032
+ if (node.kind === "fragment" || node.kind === "element") {
1033
+ return node.children.some(containsReactNodeRender);
1034
+ }
1035
+ if (node.kind === "component") {
1036
+ return (node.children.some(containsReactNodeRender) ||
1037
+ node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsReactNodeRender)));
1038
+ }
1039
+ if (node.kind === "async-boundary") {
1040
+ return (node.children.some(containsReactNodeRender) ||
1041
+ node.placeholderChildren?.some(containsReactNodeRender) === true ||
1042
+ node.catchChildren?.some(containsReactNodeRender) === true);
1043
+ }
1044
+ return false;
1045
+ }
1046
+ function containsContextProvider(node) {
1047
+ if (node.kind === "component" && node.name.endsWith(".Provider")) {
1048
+ return true;
1049
+ }
1050
+ if (node.kind === "conditional") {
1051
+ return [...node.whenTrue, ...node.whenFalse].some(containsContextProvider);
1052
+ }
1053
+ if (node.kind === "list") {
1054
+ return node.children.some(containsContextProvider);
1055
+ }
1056
+ if (node.kind === "fragment" || node.kind === "element") {
1057
+ return node.children.some(containsContextProvider);
1058
+ }
1059
+ if (node.kind === "component") {
1060
+ return (node.children.some(containsContextProvider) ||
1061
+ node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsContextProvider)));
1062
+ }
1063
+ return false;
1064
+ }
1065
+ function containsContextConsumer(node) {
1066
+ if (node.kind === "component" && node.name.endsWith(".Consumer")) {
1067
+ return true;
1068
+ }
1069
+ if (node.kind === "conditional") {
1070
+ return [...node.whenTrue, ...node.whenFalse].some(containsContextConsumer);
1071
+ }
1072
+ if (node.kind === "list") {
1073
+ return node.children.some(containsContextConsumer);
1074
+ }
1075
+ if (node.kind === "fragment" || node.kind === "element") {
1076
+ return node.children.some(containsContextConsumer);
1077
+ }
1078
+ if (node.kind === "component") {
1079
+ return (node.children.some(containsContextConsumer) ||
1080
+ node.props.some((prop) => prop.kind === "render-prop" && prop.children.some(containsContextConsumer)));
1081
+ }
1082
+ return false;
1083
+ }
1084
+ function findComponentPropCode(props, name) {
1085
+ for (const prop of props) {
1086
+ if (prop.kind === "prop" && prop.name === name) {
1087
+ return prop.code;
1088
+ }
1089
+ }
1090
+ return undefined;
1091
+ }
1092
+ function findComponentRenderProp(props, name) {
1093
+ for (const prop of props) {
1094
+ if (prop.kind === "render-prop" && prop.name === name) {
1095
+ return prop;
1096
+ }
1097
+ }
1098
+ return undefined;
1099
+ }
1100
+ function stringLiteral(value) {
1101
+ return JSON.stringify(value);
1102
+ }
1103
+ //# sourceMappingURL=emit-server.js.map