@reckona/mreact-compiler 0.0.66 → 0.0.68

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 (71) hide show
  1. package/dist/compiler-module-context.js.map +1 -1
  2. package/dist/diagnostics.js.map +1 -1
  3. package/dist/emit-client.js.map +1 -1
  4. package/dist/emit-compat.js.map +1 -1
  5. package/dist/emit-escape-helper.js.map +1 -1
  6. package/dist/emit-server-shared.js.map +1 -1
  7. package/dist/emit-server-stream.js.map +1 -1
  8. package/dist/emit-server.js.map +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal.js.map +1 -1
  11. package/dist/ir.js.map +1 -1
  12. package/dist/oxc-analysis-types.js.map +1 -1
  13. package/dist/oxc-await-analysis.js.map +1 -1
  14. package/dist/oxc-await-ids.js.map +1 -1
  15. package/dist/oxc-await-validation.js.map +1 -1
  16. package/dist/oxc-bindings.js.map +1 -1
  17. package/dist/oxc-body-lowering.js.map +1 -1
  18. package/dist/oxc-child-analysis.js.map +1 -1
  19. package/dist/oxc-code-utils.js.map +1 -1
  20. package/dist/oxc-component-detection.js.map +1 -1
  21. package/dist/oxc-component-props.js.map +1 -1
  22. package/dist/oxc-component-references.js.map +1 -1
  23. package/dist/oxc-dom-lowering.js.map +1 -1
  24. package/dist/oxc-expression-utils.js.map +1 -1
  25. package/dist/oxc-jsx-attributes.js.map +1 -1
  26. package/dist/oxc-jsx-text.js.map +1 -1
  27. package/dist/oxc-nested-lowering.js.map +1 -1
  28. package/dist/oxc-node-utils.js.map +1 -1
  29. package/dist/oxc-raw-jsx.js.map +1 -1
  30. package/dist/oxc-render-values.js.map +1 -1
  31. package/dist/oxc-runtime-emit.js.map +1 -1
  32. package/dist/oxc-transform.js.map +1 -1
  33. package/dist/oxc.js.map +1 -1
  34. package/dist/transform.js.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/package.json +4 -3
  37. package/src/compiler-module-context.ts +31 -0
  38. package/src/diagnostics.ts +184 -0
  39. package/src/emit-client.ts +837 -0
  40. package/src/emit-compat.ts +567 -0
  41. package/src/emit-escape-helper.ts +45 -0
  42. package/src/emit-server-shared.ts +384 -0
  43. package/src/emit-server-stream.ts +2558 -0
  44. package/src/emit-server.ts +1827 -0
  45. package/src/index.ts +44 -0
  46. package/src/internal.ts +1905 -0
  47. package/src/ir.ts +151 -0
  48. package/src/oxc-analysis-types.ts +5 -0
  49. package/src/oxc-await-analysis.ts +165 -0
  50. package/src/oxc-await-ids.ts +62 -0
  51. package/src/oxc-await-validation.ts +117 -0
  52. package/src/oxc-bindings.ts +70 -0
  53. package/src/oxc-body-lowering.ts +430 -0
  54. package/src/oxc-child-analysis.ts +791 -0
  55. package/src/oxc-code-utils.ts +19 -0
  56. package/src/oxc-component-detection.ts +459 -0
  57. package/src/oxc-component-props.ts +170 -0
  58. package/src/oxc-component-references.ts +613 -0
  59. package/src/oxc-dom-lowering.ts +127 -0
  60. package/src/oxc-expression-utils.ts +42 -0
  61. package/src/oxc-jsx-attributes.ts +110 -0
  62. package/src/oxc-jsx-text.ts +84 -0
  63. package/src/oxc-nested-lowering.ts +319 -0
  64. package/src/oxc-node-utils.ts +65 -0
  65. package/src/oxc-raw-jsx.ts +239 -0
  66. package/src/oxc-render-values.ts +620 -0
  67. package/src/oxc-runtime-emit.ts +212 -0
  68. package/src/oxc-transform.ts +77 -0
  69. package/src/oxc.ts +932 -0
  70. package/src/transform.ts +634 -0
  71. package/src/types.ts +117 -0
@@ -0,0 +1,837 @@
1
+ import type {
2
+ ComponentPropIr,
3
+ ComponentIr,
4
+ JsxNodeIr,
5
+ ModuleIr,
6
+ } from "./ir.js";
7
+ import type { RuntimeImport } from "./types.js";
8
+ import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
9
+
10
+ export interface EmitResult {
11
+ code: string;
12
+ imports: RuntimeImport[];
13
+ }
14
+
15
+ export function emitClient(ir: ModuleIr): EmitResult {
16
+ const imports = collectImports(ir);
17
+ const helperNames = allocateRuntimeHelperNames(ir, imports[0]?.specifiers ?? []);
18
+ const importLine =
19
+ imports[0]?.specifiers.length === 0 ? "" : emitRuntimeImportLine(imports, helperNames);
20
+ const userImports = emitUserImports(ir);
21
+ const moduleStatements = emitModuleStatements(ir);
22
+ const moduleAllocator = createNameAllocator([]);
23
+ const clientBoundaryHelperName = hasClientReferenceNodes(ir)
24
+ ? moduleAllocator("__mreactClientBoundary", ir.moduleBindingNames)
25
+ : undefined;
26
+ const clientBoundaryHelper =
27
+ clientBoundaryHelperName === undefined ? "" : emitClientBoundaryHelper(clientBoundaryHelperName);
28
+ const components = ir.components
29
+ .map((component) =>
30
+ emitComponent(component, moduleAllocator, helperNames, clientBoundaryHelperName),
31
+ )
32
+ .join("\n\n");
33
+
34
+ return {
35
+ code: `${[importLine, userImports, moduleStatements, clientBoundaryHelper].filter(Boolean).join("\n")}\n\n${components}\n`,
36
+ imports,
37
+ };
38
+ }
39
+
40
+ type RuntimeHelperName =
41
+ | "bindList"
42
+ | "bindEvent"
43
+ | "bindProp"
44
+ | "bindSpreadProps"
45
+ | "bindText"
46
+ | "createTemplate"
47
+ | "insertDynamic";
48
+
49
+ type RuntimeHelperNames = Record<RuntimeHelperName, string>;
50
+
51
+ function allocateRuntimeHelperNames(
52
+ ir: ModuleIr,
53
+ specifiers: readonly string[],
54
+ ): RuntimeHelperNames {
55
+ const allocator = createNameAllocator([
56
+ ...ir.moduleBindingNames,
57
+ ...ir.components.flatMap((component) => [
58
+ component.name,
59
+ component.exportName,
60
+ ...component.bindingNames,
61
+ ]),
62
+ ]);
63
+ const helperNames: RuntimeHelperNames = {
64
+ bindList: "bindList",
65
+ bindEvent: "bindEvent",
66
+ bindProp: "bindProp",
67
+ bindSpreadProps: "bindSpreadProps",
68
+ bindText: "bindText",
69
+ createTemplate: "createTemplate",
70
+ insertDynamic: "insertDynamic",
71
+ };
72
+
73
+ for (const specifier of specifiers) {
74
+ const helper = specifier as RuntimeHelperName;
75
+
76
+ if (ir.moduleBindingNames.includes(helper)) {
77
+ helperNames[helper] = allocator(`_${helper}`);
78
+ }
79
+ }
80
+
81
+ return helperNames;
82
+ }
83
+
84
+ function emitRuntimeImportLine(
85
+ imports: RuntimeImport[],
86
+ helperNames: RuntimeHelperNames,
87
+ ): string {
88
+ const specifiers = imports[0]?.specifiers ?? ["createTemplate"];
89
+ const importedNames = specifiers.map((specifier) => {
90
+ const helper = specifier as RuntimeHelperName;
91
+ const localName = helperNames[helper];
92
+
93
+ return localName === specifier ? specifier : `${specifier} as ${localName}`;
94
+ });
95
+
96
+ return `import { ${importedNames.join(", ")} } from "@reckona/mreact-reactive-dom";`;
97
+ }
98
+
99
+ function emitUserImports(ir: ModuleIr): string {
100
+ return ir.components.length === 0 ? "" : ir.userImports.join("\n");
101
+ }
102
+
103
+ function emitModuleStatements(ir: ModuleIr): string {
104
+ return ir.components.length === 0 ? "" : ir.moduleStatements.join("\n");
105
+ }
106
+
107
+ function collectImports(ir: ModuleIr): RuntimeImport[] {
108
+ if (ir.components.length === 0) {
109
+ return [];
110
+ }
111
+
112
+ const specifiers = new Set<string>(["createTemplate"]);
113
+
114
+ for (const component of ir.components) {
115
+ visit(component.root, (node) => {
116
+ if (node.kind === "expr") {
117
+ specifiers.add(
118
+ node.renderMode === "dynamic" ? "insertDynamic" : "bindText",
119
+ );
120
+ }
121
+
122
+ if (node.kind === "conditional") {
123
+ specifiers.add("insertDynamic");
124
+ }
125
+
126
+ if (node.kind === "list") {
127
+ specifiers.add("bindList");
128
+ }
129
+
130
+ if (node.kind === "element") {
131
+ for (const attr of node.attributes) {
132
+ if (attr.kind === "dynamic-attr") {
133
+ specifiers.add("bindProp");
134
+ }
135
+
136
+ if (attr.kind === "spread-attr") {
137
+ specifiers.add("bindSpreadProps");
138
+ }
139
+
140
+ if (attr.kind === "event") {
141
+ specifiers.add("bindEvent");
142
+ }
143
+ }
144
+ }
145
+ });
146
+ }
147
+
148
+ return [
149
+ {
150
+ source: "@reckona/mreact-reactive-dom",
151
+ specifiers: Array.from(specifiers).sort(),
152
+ },
153
+ ];
154
+ }
155
+
156
+ function hasClientReferenceNodes(ir: ModuleIr): boolean {
157
+ return ir.components.some((component) => {
158
+ let found = false;
159
+ visit(component.root, (node) => {
160
+ if (
161
+ node.kind === "component" &&
162
+ node.clientReference !== undefined &&
163
+ isCompatClientReferenceModuleId(node.clientReference.moduleId)
164
+ ) {
165
+ found = true;
166
+ }
167
+ });
168
+ return found;
169
+ });
170
+ }
171
+
172
+ function emitClientBoundaryHelper(name: string): string {
173
+ return `function ${name}(name, props) {
174
+ const fragment = document.createDocumentFragment();
175
+ const placeholder = document.createElement("template");
176
+ placeholder.setAttribute("data-mreact-client-boundary", name);
177
+ const propsElement = document.createElement("script");
178
+ propsElement.type = "application/json";
179
+ propsElement.setAttribute("data-mreact-client-boundary-props", name);
180
+ try {
181
+ propsElement.textContent = JSON.stringify(props ?? {}).replaceAll("<", "\\\\u003c");
182
+ } catch {
183
+ placeholder.setAttribute("data-mreact-client-boundary-nonserializable", "true");
184
+ propsElement.textContent = "{}";
185
+ }
186
+ fragment.append(placeholder, propsElement);
187
+ return fragment;
188
+ }`;
189
+ }
190
+
191
+ function emitComponent(
192
+ component: ComponentIr,
193
+ moduleAllocator: NameAllocator,
194
+ helperNames: RuntimeHelperNames,
195
+ clientBoundaryHelperName: string | undefined,
196
+ ): string {
197
+ const templateName = moduleAllocator(
198
+ "_tmpl_" + component.name,
199
+ component.bindingNames,
200
+ );
201
+ const allocator = createNameAllocator([...component.bindingNames, templateName]);
202
+ const body = component.bodyStatements.map((statement) => ` ${statement}`);
203
+ const parameters = component.parameters.join(", ");
204
+
205
+ if (component.root.kind === "component") {
206
+ const state = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName };
207
+ return [
208
+ `${component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
209
+ ...body,
210
+ ` return ${emitComponentCall(
211
+ component.root.name,
212
+ component.root.props,
213
+ component.root.children,
214
+ state,
215
+ component.root.clientReference === undefined
216
+ ? undefined
217
+ : { moduleId: component.root.clientReference.moduleId, name: component.root.name },
218
+ )};`,
219
+ `}`,
220
+ ].join("\n");
221
+ }
222
+
223
+ if (component.root.kind === "conditional") {
224
+ const state = { allocateName: allocator, textIndex: 0, helperNames, clientBoundaryHelperName };
225
+ const fragmentName = allocator("_fragment");
226
+ const markerName = allocator("_marker");
227
+ return [
228
+ `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
229
+ ...body,
230
+ ` const ${fragmentName} = document.createDocumentFragment();`,
231
+ ` const ${markerName} = document.createComment("");`,
232
+ ` ${fragmentName}.append(${markerName});`,
233
+ ` ${helperNames.insertDynamic}(${fragmentName}, ${markerName}, () => ${emitNodeRenderValueExpression(component.root, state)});`,
234
+ ` return ${fragmentName};`,
235
+ `}`,
236
+ ].join("\n");
237
+ }
238
+
239
+ const fragmentName = allocator("_fragment");
240
+ const rootName = allocator("_root");
241
+ const templateHtml = escapeTemplateHtml(renderStaticHtml(component.root));
242
+ const setup = emitSetup(component.root, rootName, {
243
+ allocateName: allocator,
244
+ textIndex: 0,
245
+ helperNames,
246
+ clientBoundaryHelperName,
247
+ });
248
+ return [
249
+ `const ${templateName} = ${helperNames.createTemplate}("${templateHtml}");`,
250
+ `${component.exportDefault === true ? "export default " : component.exported === false ? "" : "export "}function ${component.name}(${parameters}) {`,
251
+ ...body,
252
+ ` const ${fragmentName} = ${templateName}();`,
253
+ component.root.kind === "fragment"
254
+ ? ` const ${rootName} = ${fragmentName};`
255
+ : ` const ${rootName} = ${fragmentName}.firstChild;`,
256
+ setup,
257
+ ` return ${rootName};`,
258
+ `}`,
259
+ ]
260
+ .filter(Boolean)
261
+ .join("\n");
262
+ }
263
+
264
+ function renderStaticHtml(node: JsxNodeIr): string {
265
+ if (node.kind === "text") {
266
+ return escapeHtml(node.value);
267
+ }
268
+
269
+ if (node.kind === "expr") {
270
+ return "<!---->";
271
+ }
272
+
273
+ if (node.kind === "conditional" || node.kind === "list") {
274
+ return "<!---->";
275
+ }
276
+
277
+ if (node.kind === "fragment") {
278
+ return node.children.map(renderStaticHtml).join("");
279
+ }
280
+
281
+ if (node.kind === "component") {
282
+ return "<!---->";
283
+ }
284
+
285
+ if (node.kind === "async-boundary") {
286
+ return "<!--mreact-async-boundary-->";
287
+ }
288
+
289
+ const attrs = node.attributes
290
+ .filter((attr) => attr.kind === "static-attr")
291
+ .map((attr) => ` ${attr.name}="${escapeHtml(attr.value)}"`)
292
+ .join("");
293
+ const children = node.children.map(renderStaticHtml).join("");
294
+
295
+ return `<${node.tagName}${attrs}>${children}</${node.tagName}>`;
296
+ }
297
+
298
+ interface EmitSetupState {
299
+ allocateName: (baseName: string) => string;
300
+ textIndex: number;
301
+ helperNames: RuntimeHelperNames;
302
+ clientBoundaryHelperName?: string | undefined;
303
+ }
304
+
305
+ function emitSetup(
306
+ node: JsxNodeIr,
307
+ path: string,
308
+ state: EmitSetupState,
309
+ ): string {
310
+ const lines: string[] = [];
311
+
312
+ if (
313
+ node.kind !== "element" &&
314
+ node.kind !== "fragment" &&
315
+ node.kind !== "component"
316
+ ) {
317
+ return "";
318
+ }
319
+
320
+ if (node.kind === "component") {
321
+ const componentVar = state.allocateName("_component");
322
+ lines.push(
323
+ ` const ${componentVar} = ${emitComponentCall(
324
+ node.name,
325
+ node.props,
326
+ node.children,
327
+ state,
328
+ node.clientReference === undefined
329
+ ? undefined
330
+ : { moduleId: node.clientReference.moduleId, name: node.name },
331
+ )};`,
332
+ );
333
+ lines.push(` if (${componentVar} == null || typeof ${componentVar} === "boolean") {`);
334
+ lines.push(` ${path}.remove();`);
335
+ lines.push(` } else {`);
336
+ lines.push(` ${path}.replaceWith(${componentVar});`);
337
+ lines.push(` }`);
338
+ return lines.join("\n");
339
+ }
340
+
341
+ if (node.kind === "element") {
342
+ for (const attr of node.attributes) {
343
+ if (attr.kind === "dynamic-attr") {
344
+ lines.push(
345
+ ` ${state.helperNames.bindProp}(${path}, "${attr.name}", () => (${attr.code}));`,
346
+ );
347
+ }
348
+
349
+ if (attr.kind === "spread-attr") {
350
+ lines.push(
351
+ ` ${state.helperNames.bindSpreadProps}(${path}, () => (${attr.code}));`,
352
+ );
353
+ }
354
+
355
+ if (attr.kind === "event") {
356
+ lines.push(
357
+ ` ${state.helperNames.bindEvent}(${path}, "${attr.eventName}", ${attr.code});`,
358
+ );
359
+ }
360
+ }
361
+ }
362
+
363
+ const children = node.children;
364
+ const stableChildrenName = hasLiveChildListMutation(children)
365
+ ? state.allocateName("_children")
366
+ : undefined;
367
+ let childIndex = 0;
368
+
369
+ if (stableChildrenName !== undefined) {
370
+ lines.push(` const ${stableChildrenName} = Array.from(${path}.childNodes);`);
371
+ }
372
+
373
+ let sawStaticText = false;
374
+ let sawComponentMutation = false;
375
+
376
+ for (const child of children) {
377
+ if (child.kind === "text") {
378
+ sawStaticText = true;
379
+ childIndex += 1;
380
+ continue;
381
+ }
382
+
383
+ const childPath =
384
+ stableChildrenName === undefined ||
385
+ (
386
+ child.kind !== "component" &&
387
+ !sawComponentMutation &&
388
+ usesLiveInsertionAnchor(child) &&
389
+ !sawStaticText
390
+ )
391
+ ? `${path}.childNodes[${childIndex}]`
392
+ : `${stableChildrenName}[${childIndex}]`;
393
+
394
+ if (child.kind === "expr") {
395
+ if (child.renderMode === "dynamic") {
396
+ lines.push(
397
+ ` ${state.helperNames.insertDynamic}(${path}, ${childPath}, () => (${child.code}));`,
398
+ );
399
+ childIndex += 1;
400
+ continue;
401
+ }
402
+
403
+ const textVar = state.allocateName(`_text_${state.textIndex}`);
404
+ state.textIndex += 1;
405
+ lines.push(` const ${textVar} = document.createTextNode("");`);
406
+ lines.push(` ${childPath}.replaceWith(${textVar});`);
407
+ lines.push(
408
+ ` ${state.helperNames.bindText}(${textVar}, () => (${child.code}));`,
409
+ );
410
+ childIndex += 1;
411
+ continue;
412
+ }
413
+
414
+ if (child.kind === "conditional") {
415
+ lines.push(
416
+ ` ${state.helperNames.insertDynamic}(${path}, ${childPath}, () => (${child.conditionCode}) ? ${emitRenderValueExpression(child.whenTrue, state)} : ${emitRenderValueExpression(child.whenFalse, state)});`,
417
+ );
418
+ childIndex += 1;
419
+ continue;
420
+ }
421
+
422
+ if (child.kind === "list") {
423
+ const parameters =
424
+ child.indexName === undefined
425
+ ? child.itemName
426
+ : `${child.itemName}, ${child.indexName}`;
427
+ const optionEntries: string[] = [];
428
+
429
+ if (child.keyCode !== undefined) {
430
+ optionEntries.push(`key: (${parameters}) => (${child.keyCode})`);
431
+ }
432
+
433
+ if (
434
+ child.keyCode !== undefined &&
435
+ listReadsNestedItemObject(child, child.itemName)
436
+ ) {
437
+ optionEntries.push("nestedObjectFallback: true");
438
+ }
439
+
440
+ const options =
441
+ optionEntries.length === 0 ? "" : `, { ${optionEntries.join(", ")} }`;
442
+ lines.push(
443
+ ` ${state.helperNames.bindList}(${path}, ${childPath}, () => (${child.itemsCode}), ${emitListRenderer(child, parameters, state)}${options});`,
444
+ );
445
+ childIndex += 1;
446
+ continue;
447
+ }
448
+
449
+ if (child.kind === "async-boundary") {
450
+ lines.push(emitAsyncBoundarySetup(child, childPath, state));
451
+ childIndex += 1;
452
+ continue;
453
+ }
454
+
455
+ lines.push(emitSetup(child, childPath, state));
456
+ if (child.kind === "component") {
457
+ sawComponentMutation = true;
458
+ }
459
+ childIndex += 1;
460
+ }
461
+
462
+ return lines.filter(Boolean).join("\n");
463
+ }
464
+
465
+ function listReadsNestedItemObject(
466
+ node: Extract<JsxNodeIr, { kind: "list" }>,
467
+ itemName: string,
468
+ ): boolean {
469
+ return node.children.some((child) => nodeReadsNestedItemObject(child, itemName));
470
+ }
471
+
472
+ function nodeReadsNestedItemObject(node: JsxNodeIr, itemName: string): boolean {
473
+ switch (node.kind) {
474
+ case "element":
475
+ return (
476
+ codeReadsNestedItemObject(node.keyCode, itemName) ||
477
+ node.attributes.some((attribute) => {
478
+ if (attribute.kind === "spread-attr") {
479
+ return codeReadsNestedItemObject(attribute.code, itemName);
480
+ }
481
+
482
+ if (attribute.kind === "dynamic-attr" || attribute.kind === "event") {
483
+ return codeReadsNestedItemObject(attribute.code, itemName);
484
+ }
485
+
486
+ return false;
487
+ }) ||
488
+ node.children.some((child) => nodeReadsNestedItemObject(child, itemName))
489
+ );
490
+ case "component":
491
+ return (
492
+ codeReadsNestedItemObject(node.keyCode, itemName) ||
493
+ node.props.some((prop) => {
494
+ if (prop.kind === "spread-prop") {
495
+ return codeReadsNestedItemObject(prop.code, itemName);
496
+ }
497
+
498
+ if (prop.kind === "render-prop") {
499
+ return prop.children.some((child) => nodeReadsNestedItemObject(child, itemName));
500
+ }
501
+
502
+ return codeReadsNestedItemObject(prop.code, itemName);
503
+ }) ||
504
+ node.children.some((child) => nodeReadsNestedItemObject(child, itemName))
505
+ );
506
+ case "fragment":
507
+ return node.children.some((child) => nodeReadsNestedItemObject(child, itemName));
508
+ case "conditional":
509
+ return (
510
+ codeReadsNestedItemObject(node.conditionCode, itemName) ||
511
+ node.whenTrue.some((child) => nodeReadsNestedItemObject(child, itemName)) ||
512
+ node.whenFalse.some((child) => nodeReadsNestedItemObject(child, itemName))
513
+ );
514
+ case "list":
515
+ return (
516
+ codeReadsNestedItemObject(node.itemsCode, itemName) ||
517
+ codeReadsNestedItemObject(node.keyCode, itemName) ||
518
+ node.children.some((child) => nodeReadsNestedItemObject(child, itemName))
519
+ );
520
+ case "expr":
521
+ return codeReadsNestedItemObject(node.code, itemName);
522
+ case "async-boundary":
523
+ return (
524
+ codeReadsNestedItemObject(node.valueCode, itemName) ||
525
+ node.children.some((child) => nodeReadsNestedItemObject(child, itemName)) ||
526
+ (node.placeholderChildren?.some((child) =>
527
+ nodeReadsNestedItemObject(child, itemName),
528
+ ) ?? false) ||
529
+ (node.catchChildren?.some((child) =>
530
+ nodeReadsNestedItemObject(child, itemName),
531
+ ) ?? false)
532
+ );
533
+ case "text":
534
+ return false;
535
+ }
536
+ }
537
+
538
+ function codeReadsNestedItemObject(
539
+ code: string | undefined,
540
+ itemName: string,
541
+ ): boolean {
542
+ if (code === undefined || code.length === 0) {
543
+ return false;
544
+ }
545
+
546
+ const escapedItemName = itemName.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
547
+ return new RegExp(`\\b${escapedItemName}(?:\\.[A-Za-z_$][\\w$]*){2,}`).test(code);
548
+ }
549
+
550
+ function usesLiveInsertionAnchor(child: JsxNodeIr): boolean {
551
+ return (
552
+ child.kind === "component" ||
553
+ (child.kind === "expr" && child.renderMode === "dynamic") ||
554
+ child.kind === "conditional" ||
555
+ child.kind === "list" ||
556
+ child.kind === "async-boundary"
557
+ );
558
+ }
559
+
560
+ function hasLiveChildListMutation(children: readonly JsxNodeIr[]): boolean {
561
+ return children.some(usesLiveInsertionAnchor);
562
+ }
563
+
564
+ function emitRenderValueExpression(
565
+ children: JsxNodeIr[],
566
+ state: EmitSetupState,
567
+ ): string {
568
+ if (children.length === 0) {
569
+ return "null";
570
+ }
571
+
572
+ if (children.length === 1) {
573
+ return emitNodeRenderValueExpression(children[0] as JsxNodeIr, state);
574
+ }
575
+
576
+ return `[${children
577
+ .map((child) => emitNodeRenderValueExpression(child, state))
578
+ .join(", ")}]`;
579
+ }
580
+
581
+ function emitAsyncBoundarySetup(
582
+ node: Extract<JsxNodeIr, { kind: "async-boundary" }>,
583
+ childPath: string,
584
+ state: EmitSetupState,
585
+ ): string {
586
+ // Without a stable id the server has no way to tell the client what it
587
+ // resolved. Leave the placeholder comment in place so the server-rendered
588
+ // subtree (preserved via the hydration marker skip) remains the source of
589
+ // truth. The resolved buttons inside it stay non-interactive in that case.
590
+ if (node.awaitId === undefined) {
591
+ return "";
592
+ }
593
+
594
+ const valueName = node.valueName;
595
+ const renderChildren = emitRenderValueExpression(node.children, state);
596
+ const awaitIdLiteral = JSON.stringify(node.awaitId);
597
+
598
+ return [
599
+ ` {`,
600
+ ` const _awaitStore = globalThis.__mreactAwaitData;`,
601
+ ` const _awaitEntry = _awaitStore === undefined ? undefined : _awaitStore[${awaitIdLiteral}];`,
602
+ ` if (_awaitEntry !== undefined) {`,
603
+ ` const ${valueName} = _awaitEntry.value;`,
604
+ ` const _resolvedAwaitContent = ${renderChildren};`,
605
+ ` if (_resolvedAwaitContent != null) {`,
606
+ ` ${childPath}.replaceWith(_resolvedAwaitContent);`,
607
+ ` }`,
608
+ ` }`,
609
+ ` }`,
610
+ ].join("\n");
611
+ }
612
+
613
+ function emitNodeRenderValueExpression(
614
+ node: JsxNodeIr,
615
+ state: EmitSetupState,
616
+ ): string {
617
+ if (node.kind === "text") {
618
+ return JSON.stringify(node.value);
619
+ }
620
+
621
+ if (node.kind === "expr") {
622
+ return `(${node.code})`;
623
+ }
624
+
625
+ if (node.kind === "component") {
626
+ return emitComponentCall(
627
+ node.name,
628
+ node.props,
629
+ node.children,
630
+ state,
631
+ node.clientReference === undefined
632
+ ? undefined
633
+ : { moduleId: node.clientReference.moduleId, name: node.name },
634
+ );
635
+ }
636
+
637
+ if (node.kind === "fragment") {
638
+ return emitRenderValueExpression(node.children, state);
639
+ }
640
+
641
+ if (node.kind === "conditional") {
642
+ return `((${node.conditionCode}) ? ${emitRenderValueExpression(node.whenTrue, state)} : ${emitRenderValueExpression(node.whenFalse, state)})`;
643
+ }
644
+
645
+ if (node.kind === "list") {
646
+ const parameters =
647
+ node.indexName === undefined
648
+ ? node.itemName
649
+ : `${node.itemName}, ${node.indexName}`;
650
+ return `(${node.itemsCode}).map(${emitListRenderer(node, parameters, state)})`;
651
+ }
652
+
653
+ if (node.kind === "async-boundary") {
654
+ return "null";
655
+ }
656
+
657
+ const templateName = state.allocateName("_dynamicTemplate");
658
+ const fragmentName = state.allocateName("_dynamicFragment");
659
+ const rootName = state.allocateName("_dynamicRoot");
660
+ const templateHtml = escapeTemplateHtml(renderStaticHtml(node));
661
+ const setup = emitSetup(node, rootName, state);
662
+ const setupLines = setup === "" ? [] : setup.split("\n");
663
+
664
+ return [
665
+ "(() => {",
666
+ ` const ${templateName} = ${state.helperNames.createTemplate}("${templateHtml}");`,
667
+ ` const ${fragmentName} = ${templateName}();`,
668
+ ` const ${rootName} = ${fragmentName}.firstChild;`,
669
+ ...setupLines,
670
+ ` return ${rootName};`,
671
+ "})()",
672
+ ].join("\n");
673
+ }
674
+
675
+ function emitListRenderer(
676
+ node: Extract<JsxNodeIr, { kind: "list" }>,
677
+ parameters: string,
678
+ state: EmitSetupState,
679
+ ): string {
680
+ const valueExpression = emitRenderValueExpression(node.children, state);
681
+
682
+ if (node.bodyStatements === undefined || node.bodyStatements.length === 0) {
683
+ return `(${parameters}) => ${valueExpression}`;
684
+ }
685
+
686
+ return `(${parameters}) => {\n${node.bodyStatements.map((statement) => ` ${statement}`).join("\n")}\n return ${valueExpression};\n }`;
687
+ }
688
+
689
+ function emitComponentCall(
690
+ name: string,
691
+ props: ComponentPropIr[],
692
+ children: JsxNodeIr[],
693
+ state: EmitSetupState,
694
+ clientReference?: { moduleId: string; name: string } | undefined,
695
+ ): string {
696
+ if (
697
+ clientReference !== undefined &&
698
+ state.clientBoundaryHelperName !== undefined &&
699
+ isCompatClientReferenceModuleId(clientReference.moduleId)
700
+ ) {
701
+ return `${state.clientBoundaryHelperName}(${JSON.stringify(clientReference.name)}, ${emitPropsObject(props, children, state)})`;
702
+ }
703
+
704
+ return `${name}(${emitPropsObject(props, children, state)})`;
705
+ }
706
+
707
+ function emitPropsObject(
708
+ props: ComponentPropIr[],
709
+ children: JsxNodeIr[],
710
+ state: EmitSetupState,
711
+ ): string {
712
+ const entries = props.map((prop) => {
713
+ if (prop.kind === "spread-prop") {
714
+ return `...(${prop.code})`;
715
+ }
716
+
717
+ if (prop.kind === "render-prop") {
718
+ return `${emitPropName(prop.name)}: ${emitRenderValueExpression(prop.children, state)}`;
719
+ }
720
+
721
+ if (shouldEmitReactiveComponentPropGetter(prop.code)) {
722
+ return `get ${emitGetterPropName(prop.name)}() { return (${prop.code}); }`;
723
+ }
724
+
725
+ return `${emitPropName(prop.name)}: (${prop.code})`;
726
+ });
727
+
728
+ if (children.length > 0) {
729
+ entries.push(`children: ${emitRenderValueExpression(children, state)}`);
730
+ }
731
+
732
+ return `{ ${entries.join(", ")} }`;
733
+ }
734
+
735
+ function emitPropName(name: string): string {
736
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
737
+ }
738
+
739
+ function emitGetterPropName(name: string): string {
740
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : `[${JSON.stringify(name)}]`;
741
+ }
742
+
743
+ function shouldEmitReactiveComponentPropGetter(code: string): boolean {
744
+ if (!/\.\s*get\s*\(/.test(code)) {
745
+ return false;
746
+ }
747
+
748
+ return !/^\s*(?:async\s*)?(?:function\b|(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)/.test(code);
749
+ }
750
+
751
+ function createNameAllocator(
752
+ reservedNames: readonly string[],
753
+ ): NameAllocator {
754
+ const usedNames = new Set(reservedNames);
755
+
756
+ return (baseName: string, extraReservedNames: readonly string[] = []): string => {
757
+ const reservedNames = new Set(extraReservedNames);
758
+ let name = baseName;
759
+ let index = 1;
760
+
761
+ while (usedNames.has(name) || reservedNames.has(name)) {
762
+ name = `${baseName}$${index}`;
763
+ index += 1;
764
+ }
765
+
766
+ usedNames.add(name);
767
+ return name;
768
+ };
769
+ }
770
+
771
+ function isCompatClientReferenceModuleId(moduleId: string): boolean {
772
+ return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(moduleId);
773
+ }
774
+
775
+ type NameAllocator = (
776
+ baseName: string,
777
+ extraReservedNames?: readonly string[],
778
+ ) => string;
779
+
780
+ function visit(node: JsxNodeIr, fn: (node: JsxNodeIr) => void): void {
781
+ fn(node);
782
+
783
+ if (node.kind === "conditional") {
784
+ for (const child of [...node.whenTrue, ...node.whenFalse]) {
785
+ visit(child, fn);
786
+ }
787
+ }
788
+
789
+ if (node.kind === "list") {
790
+ for (const child of node.children) {
791
+ visit(child, fn);
792
+ }
793
+ }
794
+
795
+ if (node.kind === "component") {
796
+ for (const prop of node.props) {
797
+ if (prop.kind === "render-prop") {
798
+ for (const child of prop.children) {
799
+ visit(child, fn);
800
+ }
801
+ }
802
+ }
803
+
804
+ for (const child of node.children) {
805
+ visit(child, fn);
806
+ }
807
+ }
808
+
809
+ if (node.kind === "element" || node.kind === "fragment") {
810
+ for (const child of node.children) {
811
+ visit(child, fn);
812
+ }
813
+ }
814
+
815
+ // Async-boundary children participate in client-side rendering when the
816
+ // boundary has an awaitId (hydration data path). Traverse them so their
817
+ // runtime imports (bindList / bindText / bindEvent / etc.) are included.
818
+ if (node.kind === "async-boundary") {
819
+ for (const child of node.children) {
820
+ visit(child, fn);
821
+ }
822
+ if (node.placeholderChildren !== undefined) {
823
+ for (const child of node.placeholderChildren) {
824
+ visit(child, fn);
825
+ }
826
+ }
827
+ if (node.catchChildren !== undefined) {
828
+ for (const child of node.catchChildren) {
829
+ visit(child, fn);
830
+ }
831
+ }
832
+ }
833
+ }
834
+
835
+ function escapeTemplateHtml(value: string): string {
836
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
837
+ }