@barefootjs/jsx 0.26.2 → 0.26.3

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 (80) hide show
  1. package/dist/adapters/interface.d.ts +29 -0
  2. package/dist/adapters/interface.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts +9 -0
  4. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  5. package/dist/adapters/loop-bound-names.d.ts.map +1 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
  7. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  8. package/dist/adapters/test-adapter.d.ts.map +1 -1
  9. package/dist/analyzer-context.d.ts +11 -1
  10. package/dist/analyzer-context.d.ts.map +1 -1
  11. package/dist/analyzer.d.ts +40 -1
  12. package/dist/analyzer.d.ts.map +1 -1
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.js +662 -223
  15. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +2 -0
  17. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +12 -0
  25. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
  28. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/html-template.d.ts +19 -1
  31. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
  33. package/dist/ir-to-client-js/types.d.ts +4 -4
  34. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  35. package/dist/jsx-to-ir.d.ts.map +1 -1
  36. package/dist/loop-destructure.d.ts.map +1 -1
  37. package/dist/strip-types.d.ts +18 -0
  38. package/dist/strip-types.d.ts.map +1 -1
  39. package/dist/types.d.ts +96 -32
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +94 -0
  43. package/src/__tests__/client-js-generation.test.ts +10 -3
  44. package/src/__tests__/compiler-stress-1244.test.ts +12 -1
  45. package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
  46. package/src/__tests__/flatmap-segments.test.ts +80 -0
  47. package/src/__tests__/map-arbitrary-body.test.ts +226 -0
  48. package/src/__tests__/map-body-no-silent-divergence.test.ts +360 -0
  49. package/src/__tests__/map-multi-return-body.test.ts +172 -0
  50. package/src/__tests__/unsupported-expression.test.ts +20 -2
  51. package/src/adapters/interface.ts +40 -0
  52. package/src/adapters/jsx-adapter.ts +10 -0
  53. package/src/adapters/loop-bound-names.ts +6 -2
  54. package/src/adapters/parsed-expr-emitter.ts +5 -10
  55. package/src/adapters/test-adapter.ts +10 -0
  56. package/src/analyzer-context.ts +35 -1
  57. package/src/analyzer.ts +162 -24
  58. package/src/compiler.ts +2 -2
  59. package/src/expression-parser.ts +27 -19
  60. package/src/ir-to-client-js/build-references.ts +3 -2
  61. package/src/ir-to-client-js/collect-elements.ts +3 -3
  62. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +2 -0
  63. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +7 -2
  64. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
  65. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
  66. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +16 -3
  67. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
  68. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +17 -2
  69. package/src/ir-to-client-js/control-flow/plan/loop.ts +12 -0
  70. package/src/ir-to-client-js/control-flow/shared.ts +2 -1
  71. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +9 -2
  72. package/src/ir-to-client-js/control-flow.ts +11 -0
  73. package/src/ir-to-client-js/html-template.ts +133 -37
  74. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
  75. package/src/ir-to-client-js/types.ts +4 -3
  76. package/src/jsx-to-ir.ts +690 -164
  77. package/src/loop-destructure.ts +9 -5
  78. package/src/rich-type-refusal.ts +6 -2
  79. package/src/strip-types.ts +47 -0
  80. package/src/types.ts +110 -35
package/dist/index.js CHANGED
@@ -250,6 +250,7 @@ var UNSUPPORTED_METHODS = new Set([
250
250
  "some",
251
251
  "forEach",
252
252
  "flatMap",
253
+ "fill",
253
254
  "charAt",
254
255
  "charCodeAt",
255
256
  "codePointAt",
@@ -3380,6 +3381,41 @@ function buildSpreadAttrsMergeCall(args) {
3380
3381
  function itemAnchorTemplate(keyExpr) {
3381
3382
  return `<!--${loopItemMarker("${" + keyExpr + "}")}-->`;
3382
3383
  }
3384
+ function renderPreamble(preamble, opts) {
3385
+ let out = "";
3386
+ for (const seg of preamble.segments) {
3387
+ if (seg.kind === "js") {
3388
+ const text = opts.textVariant === "template" ? seg.templateText ?? seg.text : seg.text;
3389
+ out += opts.transformJs ? opts.transformJs(text) : text;
3390
+ } else {
3391
+ out += "`" + opts.renderLeaf(escapeLeafTextExpressions(seg.ir)) + "`";
3392
+ }
3393
+ }
3394
+ return out;
3395
+ }
3396
+ function escapeLeafTextExpressions(ir) {
3397
+ switch (ir.type) {
3398
+ case "element":
3399
+ return { ...ir, children: ir.children.map(escapeLeafTextExpressions) };
3400
+ case "fragment":
3401
+ return { ...ir, children: ir.children.map(escapeLeafTextExpressions) };
3402
+ case "expression": {
3403
+ if (ir.expr === "null" || ir.expr === "undefined")
3404
+ return ir;
3405
+ if (ir.slotId || ir.expr.trimStart().startsWith("escapeText("))
3406
+ return ir;
3407
+ return { ...ir, expr: `escapeText((${ir.expr}))`, templateExpr: ir.templateExpr ? `escapeText((${ir.templateExpr}))` : ir.templateExpr };
3408
+ }
3409
+ case "conditional":
3410
+ return {
3411
+ ...ir,
3412
+ whenTrue: escapeLeafTextExpressions(ir.whenTrue),
3413
+ whenFalse: ir.whenFalse ? escapeLeafTextExpressions(ir.whenFalse) : ir.whenFalse
3414
+ };
3415
+ default:
3416
+ return ir;
3417
+ }
3418
+ }
3383
3419
  function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, branchSlotsVar, insideLoop = false, inHoistedChildren = false) {
3384
3420
  const recurse = (n) => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, inHoistedChildren);
3385
3421
  const wrapExpr = (expr) => wrapExprWithLoopParams(expr, loopParams);
@@ -3421,15 +3457,17 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
3421
3457
  }
3422
3458
  case "text":
3423
3459
  return escapeHtml(node.value);
3424
- case "expression":
3460
+ case "expression": {
3425
3461
  if (node.expr === "null" || node.expr === "undefined")
3426
3462
  return "";
3463
+ const inner = wrapInterpolation(wrapExpr(node.expr));
3464
+ const valueExpr = node.joinArrayChild ? `Array.isArray(${inner}) ? ${inner}.join('') : (${inner} ?? '')` : inner;
3427
3465
  if (node.slotId) {
3428
- const inner = wrapInterpolation(wrapExpr(node.expr));
3429
- const slotted = branchSlotsVar ? inner : escapeTextSlotExpr(inner);
3466
+ const slotted = branchSlotsVar || node.joinArrayChild ? valueExpr : escapeTextSlotExpr(valueExpr);
3430
3467
  return `<!--bf:${node.slotId}-->\${${slotted}}<!--/-->`;
3431
3468
  }
3432
- return `\${${wrapInterpolation(wrapExpr(node.expr))}}`;
3469
+ return `\${${valueExpr}}`;
3470
+ }
3433
3471
  case "conditional": {
3434
3472
  const trueBranch = recurse(node.whenTrue);
3435
3473
  const falseBranch = recurse(node.whenFalse);
@@ -3488,14 +3526,13 @@ function irToHtmlTemplate(node, restSpreadNames, loopDepth = 0, loopParams, bran
3488
3526
  const iterMethod = node.method ?? "map";
3489
3527
  let mapExpr;
3490
3528
  if (node.flatMapCallback) {
3491
- let body = node.flatMapCallback.templateBody ?? node.flatMapCallback.body;
3492
- for (const frag of node.flatMapCallback.fragments) {
3493
- const renderedIr = irToHtmlTemplate(frag.ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop);
3494
- body = body.replace(frag.placeholder, `\`${renderedIr}\``);
3495
- }
3529
+ const body = renderPreamble(node.flatMapCallback, {
3530
+ renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop)
3531
+ });
3496
3532
  mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`;
3497
- } else if (node.mapPreamble) {
3498
- mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${node.mapPreamble} return \`${childTemplate}\` }).join('')}`;
3533
+ } else if (node.preamble) {
3534
+ const preamble = renderPreamble(node.preamble, { textVariant: "client", renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop) });
3535
+ mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
3499
3536
  } else {
3500
3537
  mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
3501
3538
  }
@@ -3743,13 +3780,16 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
3743
3780
  }
3744
3781
  case "text":
3745
3782
  return escapeHtml(node.value);
3746
- case "expression":
3783
+ case "expression": {
3747
3784
  if (node.expr === "null" || node.expr === "undefined")
3748
3785
  return "";
3786
+ const wrapped = wrapExpr(node.expr);
3787
+ const value = node.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : wrapped;
3749
3788
  if (node.slotId) {
3750
- return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(wrapExpr(node.expr))}}<!--/-->`;
3789
+ return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`;
3751
3790
  }
3752
- return `\${${wrapExpr(node.expr)}}`;
3791
+ return `\${${value}}`;
3792
+ }
3753
3793
  case "conditional": {
3754
3794
  const trueBranch = recurse(node.whenTrue);
3755
3795
  const falseBranch = recurse(node.whenFalse);
@@ -3776,14 +3816,13 @@ function irToPlaceholderTemplate(node, restSpreadNames, loopDepth = 0, loopParam
3776
3816
  const iterMethod = node.method ?? "map";
3777
3817
  let mapExpr;
3778
3818
  if (node.flatMapCallback) {
3779
- let body = node.flatMapCallback.templateBody ?? node.flatMapCallback.body;
3780
- for (const frag of node.flatMapCallback.fragments) {
3781
- const renderedIr = irToPlaceholderTemplate(frag.ir, restSpreadNames, loopDepth + 1, loopParams);
3782
- body = body.replace(frag.placeholder, `\`${renderedIr}\``);
3783
- }
3819
+ const body = renderPreamble(node.flatMapCallback, {
3820
+ renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams)
3821
+ });
3784
3822
  mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`;
3785
- } else if (node.mapPreamble) {
3786
- mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${node.mapPreamble} return \`${childTemplate}\` }).join('')}`;
3823
+ } else if (node.preamble) {
3824
+ const preamble = renderPreamble(node.preamble, { textVariant: "client", renderLeaf: (ir) => irToPlaceholderTemplate(ir, restSpreadNames, loopDepth + 1, loopParams) });
3825
+ mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
3787
3826
  } else {
3788
3827
  mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
3789
3828
  }
@@ -3971,13 +4010,16 @@ function irToComponentTemplateWithOpts(node, opts) {
3971
4010
  }
3972
4011
  case "text":
3973
4012
  return escapeHtml(node.value);
3974
- case "expression":
4013
+ case "expression": {
3975
4014
  if (node.expr === "null" || node.expr === "undefined")
3976
4015
  return "";
4016
+ const wrapped = transformExpr(node.expr, node.templateExpr);
4017
+ const value = node.joinArrayChild ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')` : wrapped;
3977
4018
  if (node.slotId) {
3978
- return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(transformExpr(node.expr, node.templateExpr))}}<!--/-->`;
4019
+ return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(wrapped)}}<!--/-->`;
3979
4020
  }
3980
- return `\${${transformExpr(node.expr, node.templateExpr)}}`;
4021
+ return `\${${value}}`;
4022
+ }
3981
4023
  case "conditional": {
3982
4024
  if (node.clientOnly && node.slotId) {
3983
4025
  return `<!--bf-cond-start:${node.slotId}--><!--bf-cond-end:${node.slotId}-->`;
@@ -4298,10 +4340,11 @@ function generateCsrTemplateWithOpts(node, opts) {
4298
4340
  {
4299
4341
  const transformed = transformExpr(node.expr, node.templateExpr);
4300
4342
  const expr = transformed === UNSAFE_TEMPLATE_EXPR ? "''" : transformed;
4343
+ const value = node.joinArrayChild ? `Array.isArray(${expr}) ? ${expr}.join('') : (${expr} ?? '')` : expr;
4301
4344
  if (node.slotId) {
4302
- return `<!--bf:${node.slotId}-->\${${escapeTextSlotExpr(expr)}}<!--/-->`;
4345
+ return `<!--bf:${node.slotId}-->\${${node.joinArrayChild ? value : escapeTextSlotExpr(expr)}}<!--/-->`;
4303
4346
  }
4304
- return `\${${expr}}`;
4347
+ return `\${${value}}`;
4305
4348
  }
4306
4349
  case "conditional": {
4307
4350
  if (node.clientOnly && node.slotId) {
@@ -4397,16 +4440,14 @@ function generateCsrTemplateWithOpts(node, opts) {
4397
4440
  const iterMethod = node.method ?? "map";
4398
4441
  let mapExpr;
4399
4442
  if (node.flatMapCallback) {
4400
- let body = node.flatMapCallback.templateBody ?? node.flatMapCallback.body;
4401
- for (const frag of node.flatMapCallback.fragments) {
4402
- const renderedIr = recurseInLoopBody(frag.ir);
4403
- body = body.replace(frag.placeholder, `\`${renderedIr}\``);
4404
- }
4405
- body = applyPropsRewrite(body, propsObjectName ?? null);
4443
+ const body = renderPreamble(node.flatMapCallback, {
4444
+ textVariant: "template",
4445
+ transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null),
4446
+ renderLeaf: (ir) => recurseInLoopBody(ir)
4447
+ });
4406
4448
  mapExpr = `\${${iterArrayExpr}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`;
4407
- } else if (node.mapPreamble) {
4408
- const rawPreamble = node.templateMapPreamble ?? node.mapPreamble;
4409
- const preamble = applyPropsRewrite(rawPreamble, propsObjectName ?? null);
4449
+ } else if (node.preamble) {
4450
+ const preamble = renderPreamble(node.preamble, { textVariant: "template", transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null), renderLeaf: (ir) => recurseInLoopBody(ir) });
4410
4451
  mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`;
4411
4452
  } else {
4412
4453
  mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`;
@@ -4561,6 +4602,39 @@ function reconstructWithoutTypes(node, sourceFile, ranges) {
4561
4602
  }
4562
4603
  return result;
4563
4604
  }
4605
+ function reconstructAsSegments(node, sourceFile, ranges, markers) {
4606
+ const nodeStart = node.getStart(sourceFile);
4607
+ const nodeEnd = node.getEnd();
4608
+ const fullText = sourceFile.text;
4609
+ const edits = [];
4610
+ for (const r of ranges) {
4611
+ if (r.end <= nodeStart || r.start >= nodeEnd)
4612
+ continue;
4613
+ edits.push({ start: r.start, end: r.end, marker: null });
4614
+ }
4615
+ markers.forEach((m, i) => edits.push({ start: m.start, end: m.end, marker: i }));
4616
+ edits.sort((a, b) => a.start - b.start || b.end - a.end);
4617
+ const segments = [];
4618
+ let jsBuf = "";
4619
+ let pos = nodeStart;
4620
+ for (const edit of edits) {
4621
+ if (edit.start < pos)
4622
+ continue;
4623
+ jsBuf += fullText.slice(pos, edit.start);
4624
+ if (edit.marker !== null) {
4625
+ if (jsBuf)
4626
+ segments.push({ js: jsBuf });
4627
+ jsBuf = "";
4628
+ segments.push({ marker: edit.marker });
4629
+ }
4630
+ pos = edit.end;
4631
+ }
4632
+ if (pos < nodeEnd)
4633
+ jsBuf += fullText.slice(pos, nodeEnd);
4634
+ if (jsBuf)
4635
+ segments.push({ js: jsBuf });
4636
+ return segments;
4637
+ }
4564
4638
  function mergeRanges(ranges) {
4565
4639
  if (ranges.length === 0)
4566
4640
  return [];
@@ -4706,10 +4780,11 @@ function findAngleBracketAfter(lastTypeArg, fullText) {
4706
4780
  }
4707
4781
 
4708
4782
  // src/analyzer-context.ts
4709
- function createAnalyzerContext(sourceFile, filePath) {
4783
+ function createAnalyzerContext(sourceFile, filePath, acceptsCallbackBody) {
4710
4784
  return {
4711
4785
  sourceFile,
4712
4786
  filePath,
4787
+ acceptsCallbackBody,
4713
4788
  componentName: null,
4714
4789
  componentNode: null,
4715
4790
  hasDefaultExport: false,
@@ -4755,6 +4830,9 @@ function createAnalyzerContext(sourceFile, filePath) {
4755
4830
  } catch {
4756
4831
  ownSourceFile = undefined;
4757
4832
  }
4833
+ if (process.env.BF_ASSERT_NO_JSX_IN_GETJS === "1" && this.errors.length === 0 && nodeContainsJsx(node)) {
4834
+ throw new Error("getJS() called on a JSX-bearing node — raw JSX must never be spliced " + "into emitted output. Carry mixed content as structured segments " + "(MapCallbackPreamble / FlatMapCallback) instead.");
4835
+ }
4758
4836
  if (ownSourceFile && ownSourceFile !== sourceFile) {
4759
4837
  return node.getText(ownSourceFile);
4760
4838
  }
@@ -4762,6 +4840,11 @@ function createAnalyzerContext(sourceFile, filePath) {
4762
4840
  }
4763
4841
  };
4764
4842
  }
4843
+ function nodeContainsJsx(node) {
4844
+ if (ts7.isJsxElement(node) || ts7.isJsxSelfClosingElement(node) || ts7.isJsxFragment(node))
4845
+ return true;
4846
+ return ts7.forEachChild(node, nodeContainsJsx) ?? false;
4847
+ }
4765
4848
  function getSourceLocation(node, sourceFile, filePath) {
4766
4849
  const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
4767
4850
  const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
@@ -5564,7 +5647,7 @@ function createProgramForFile(source, filePath) {
5564
5647
  return null;
5565
5648
  }
5566
5649
  }
5567
- function analyzeComponent(source, filePath, targetComponentName, program) {
5650
+ function analyzeComponent(source, filePath, targetComponentName, program, acceptsCallbackBody) {
5568
5651
  incrementCounter("filesAnalyzed");
5569
5652
  const hadSharedProgram = program !== undefined;
5570
5653
  const prescan = prescanReactiveFactoriesInSource(source, filePath);
@@ -5597,7 +5680,7 @@ function analyzeComponent(source, filePath, targetComponentName, program) {
5597
5680
  checker = result.checker;
5598
5681
  }
5599
5682
  }
5600
- const ctx = createAnalyzerContext(sourceFile, filePath);
5683
+ const ctx = createAnalyzerContext(sourceFile, filePath, acceptsCallbackBody);
5601
5684
  ctx.checker = checker;
5602
5685
  ctx.reactiveFactories = prescan.factories;
5603
5686
  ctx.declinedReactiveFactories = prescan.declined;
@@ -6649,9 +6732,15 @@ function extractSingleJsxReturn(body) {
6649
6732
  return null;
6650
6733
  return jsxReturn;
6651
6734
  }
6652
- function extractMultiReturnJsxBranches(body) {
6735
+ function preambleUnsafe(preamble, branches, fallback) {
6736
+ if (preamble.length === 0)
6737
+ return false;
6738
+ return fallback === null || branches.some((b) => b.jsxReturn === null);
6739
+ }
6740
+ function extractMultiReturnJsxBranches(body, allowPreamble = false) {
6653
6741
  const branches = [];
6654
6742
  let fallback = null;
6743
+ const preamble = [];
6655
6744
  const stmts = body.statements;
6656
6745
  for (let i = 0;i < stmts.length; i++) {
6657
6746
  const stmt = stmts[i];
@@ -6681,7 +6770,9 @@ function extractMultiReturnJsxBranches(body) {
6681
6770
  }
6682
6771
  if (branches.length === 0)
6683
6772
  return null;
6684
- return { branches, fallback };
6773
+ if (preambleUnsafe(preamble, branches, fallback))
6774
+ return null;
6775
+ return { branches, fallback, preamble };
6685
6776
  }
6686
6777
  break;
6687
6778
  }
@@ -6696,23 +6787,38 @@ function extractMultiReturnJsxBranches(body) {
6696
6787
  const hasDefault = stmt.caseBlock.clauses.some((c) => ts8.isDefaultClause(c));
6697
6788
  if (!hasDefault)
6698
6789
  return null;
6790
+ let pendingCases = [];
6699
6791
  for (const clause of stmt.caseBlock.clauses) {
6792
+ if (ts8.isCaseClause(clause) && clause.statements.length === 0) {
6793
+ pendingCases.push(clause.expression);
6794
+ continue;
6795
+ }
6700
6796
  const jsxReturn = findJsxReturnInCaseClause(clause);
6701
6797
  const nullReturn = findNullReturnInCaseClause(clause);
6702
6798
  if (!jsxReturn && !nullReturn)
6703
6799
  return null;
6800
+ if (!caseClauseIsDirectReturn(clause))
6801
+ return null;
6704
6802
  if (ts8.isCaseClause(clause)) {
6705
6803
  branches.push({
6706
6804
  condition: clause.expression,
6707
- jsxReturn: jsxReturn ?? null
6805
+ jsxReturn: jsxReturn ?? null,
6806
+ extraCaseConditions: pendingCases.length > 0 ? pendingCases : undefined
6708
6807
  });
6808
+ pendingCases = [];
6709
6809
  } else {
6810
+ if (pendingCases.length > 0)
6811
+ return null;
6710
6812
  fallback = jsxReturn ?? null;
6711
6813
  }
6712
6814
  }
6815
+ if (pendingCases.length > 0)
6816
+ return null;
6713
6817
  if (branches.length === 0)
6714
6818
  return null;
6715
- return { branches, fallback, switchDiscriminant: stmt.expression };
6819
+ if (preambleUnsafe(preamble, branches, fallback))
6820
+ return null;
6821
+ return { branches, fallback, switchDiscriminant: stmt.expression, preamble };
6716
6822
  }
6717
6823
  if (ts8.isReturnStatement(stmt) && stmt.expression) {
6718
6824
  const expr = unwrapJsxTransparent(stmt.expression);
@@ -6723,13 +6829,22 @@ function extractMultiReturnJsxBranches(body) {
6723
6829
  }
6724
6830
  continue;
6725
6831
  }
6726
- if (ts8.isVariableStatement(stmt))
6832
+ if (ts8.isVariableStatement(stmt)) {
6833
+ const declFlags = stmt.declarationList.flags;
6834
+ const isConstOrLet = (declFlags & ts8.NodeFlags.Const) !== 0 || (declFlags & ts8.NodeFlags.Let) !== 0;
6835
+ if (allowPreamble && isConstOrLet && branches.length === 0 && fallback === null) {
6836
+ preamble.push(stmt);
6837
+ continue;
6838
+ }
6727
6839
  return null;
6840
+ }
6728
6841
  return null;
6729
6842
  }
6730
6843
  if (branches.length === 0)
6731
6844
  return null;
6732
- return { branches, fallback };
6845
+ if (preambleUnsafe(preamble, branches, fallback))
6846
+ return null;
6847
+ return { branches, fallback, preamble };
6733
6848
  }
6734
6849
  function isDirectReturnBlock(node) {
6735
6850
  if (ts8.isReturnStatement(node))
@@ -6739,9 +6854,9 @@ function isDirectReturnBlock(node) {
6739
6854
  for (const stmt of node.statements) {
6740
6855
  if (ts8.isReturnStatement(stmt)) {
6741
6856
  returnCount++;
6742
- } else if (ts8.isIfStatement(stmt) || ts8.isSwitchStatement(stmt) || ts8.isForStatement(stmt) || ts8.isForOfStatement(stmt) || ts8.isForInStatement(stmt) || ts8.isWhileStatement(stmt) || ts8.isDoStatement(stmt) || ts8.isTryStatement(stmt)) {
6743
- return false;
6857
+ continue;
6744
6858
  }
6859
+ return false;
6745
6860
  }
6746
6861
  return returnCount === 1;
6747
6862
  }
@@ -6772,6 +6887,24 @@ function findJsxReturnInCaseClause(clause) {
6772
6887
  }
6773
6888
  return null;
6774
6889
  }
6890
+ function caseClauseIsDirectReturn(clause) {
6891
+ let returnCount = 0;
6892
+ let seenReturn = false;
6893
+ for (const stmt of clause.statements) {
6894
+ if (ts8.isReturnStatement(stmt)) {
6895
+ returnCount++;
6896
+ seenReturn = true;
6897
+ continue;
6898
+ }
6899
+ if (ts8.isBreakStatement(stmt)) {
6900
+ if (!seenReturn)
6901
+ return false;
6902
+ continue;
6903
+ }
6904
+ return false;
6905
+ }
6906
+ return returnCount === 1;
6907
+ }
6775
6908
  function findNullReturnInCaseClause(clause) {
6776
6909
  for (const stmt of clause.statements) {
6777
6910
  if (ts8.isReturnStatement(stmt) && stmt.expression) {
@@ -8812,6 +8945,23 @@ var REACTIVE_BINDING_KINDS = new Set([
8812
8945
  function isReactiveOrigin(origin) {
8813
8946
  return origin.freeRefs?.some((r) => REACTIVE_BINDING_KINDS.has(r.kind)) ?? false;
8814
8947
  }
8948
+ function tsxSourceText(raw) {
8949
+ return raw;
8950
+ }
8951
+ function preambleAnalysisText(p) {
8952
+ let out = "";
8953
+ for (const seg of p.segments)
8954
+ if (seg.kind === "js")
8955
+ out += seg.text;
8956
+ return out;
8957
+ }
8958
+ function preambleAnalysisTemplateText(p) {
8959
+ let out = "";
8960
+ for (const seg of p.segments)
8961
+ if (seg.kind === "js")
8962
+ out += seg.templateText ?? seg.text;
8963
+ return out;
8964
+ }
8815
8965
  var AttrValueOf = {
8816
8966
  literal(value) {
8817
8967
  return { kind: "literal", value };
@@ -10051,8 +10201,13 @@ function attachParsedExpressions(node, analyzer, bound = EMPTY_BOUND) {
10051
10201
  for (const child of nested.children)
10052
10202
  attachParsedExpressions(child, analyzer, loopBound);
10053
10203
  }
10054
- for (const frag of node.flatMapCallback?.fragments ?? []) {
10055
- attachParsedExpressions(frag.ir, analyzer, loopBound);
10204
+ for (const seg of node.flatMapCallback?.segments ?? []) {
10205
+ if (seg.kind === "jsx")
10206
+ attachParsedExpressions(seg.ir, analyzer, loopBound);
10207
+ }
10208
+ for (const seg of node.preamble?.segments ?? []) {
10209
+ if (seg.kind === "jsx")
10210
+ attachParsedExpressions(seg.ir, analyzer, loopBound);
10056
10211
  }
10057
10212
  break;
10058
10213
  }
@@ -10687,74 +10842,78 @@ function transformMultiReturnJsxFunctionCall(callExpr, info, ctx) {
10687
10842
  ctx.analyzer.getJS = substitutedGetJS;
10688
10843
  try {
10689
10844
  const loc = getSourceLocation(callExpr, ctx.sourceFile, ctx.filePath);
10690
- const nullExpr = {
10691
- type: "expression",
10692
- expr: "null",
10693
- typeInfo: { kind: "primitive", raw: "null", primitive: "null" },
10694
- reactive: false,
10695
- slotId: null,
10696
- loc,
10697
- origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
10698
- };
10699
- let result = info.fallback ? transformNode(info.fallback, ctx) ?? nullExpr : nullExpr;
10700
- for (let i = info.branches.length - 1;i >= 0; i--) {
10701
- const branch = info.branches[i];
10702
- let conditionText;
10703
- if (info.switchDiscriminant) {
10704
- const discText = substitutedGetJS(info.switchDiscriminant);
10705
- const caseText = substitutedGetJS(branch.condition);
10706
- conditionText = `${discText} === ${caseText}`;
10707
- } else {
10708
- conditionText = substitutedGetJS(branch.condition);
10709
- }
10710
- const env = makeBindingEnv(ctx);
10711
- const caseFreeRefs = resolveFreeRefs(branch.condition, env);
10712
- const discFreeRefs = info.switchDiscriminant ? resolveFreeRefs(info.switchDiscriminant, env) : [];
10713
- const conditionOrigin = {
10714
- phase: "tick",
10715
- scope: "template",
10716
- effect: "pure",
10717
- freeRefs: [...discFreeRefs, ...caseFreeRefs]
10718
- };
10719
- const reactive = isReactiveExpression(conditionText, ctx, branch.condition) || isReactiveOrigin(conditionOrigin);
10720
- const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx);
10721
- const callsReactive = exprCallsReactiveGetters(branch.condition, ctx) || (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx) : false);
10722
- const hasCalls = exprHasFunctionCalls(branch.condition) || (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false);
10723
- const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls;
10724
- const slotId = needsSlot ? generateSlotId(ctx) : null;
10725
- const whenTrue = branch.jsxReturn ? transformNode(branch.jsxReturn, ctx) ?? nullExpr : nullExpr;
10726
- let templateCondition;
10727
- if (info.switchDiscriminant) {
10728
- const discRewritten = rewriteBarePropRefs2(substitutedGetJS(info.switchDiscriminant), info.switchDiscriminant, ctx);
10729
- const caseRewritten = rewriteBarePropRefs2(substitutedGetJS(branch.condition), branch.condition, ctx);
10730
- const discPart = discRewritten ?? substitutedGetJS(info.switchDiscriminant);
10731
- const casePart = caseRewritten ?? substitutedGetJS(branch.condition);
10732
- templateCondition = `${discPart} === ${casePart}`;
10733
- } else {
10734
- templateCondition = rewriteBarePropRefs2(conditionText, branch.condition, ctx);
10735
- }
10736
- const conditional = {
10737
- type: "conditional",
10738
- condition: conditionText,
10739
- templateCondition,
10740
- conditionType: null,
10741
- reactive,
10742
- whenTrue,
10743
- whenFalse: result,
10744
- slotId,
10745
- callsReactiveGetters: callsReactive || undefined,
10746
- hasFunctionCalls: hasCalls || undefined,
10747
- loc,
10748
- origin: conditionOrigin
10749
- };
10750
- result = conditional;
10751
- }
10752
- return result;
10845
+ return foldMultiReturnBranches(info, ctx, loc, substitutedGetJS);
10753
10846
  } finally {
10754
10847
  ctx.getJS = originalCtxGetJS;
10755
10848
  ctx.analyzer.getJS = originalAnalyzerGetJS;
10756
10849
  }
10757
10850
  }
10851
+ function foldMultiReturnBranches(info, ctx, loc, getText) {
10852
+ const nullExpr = {
10853
+ type: "expression",
10854
+ expr: "null",
10855
+ typeInfo: { kind: "primitive", raw: "null", primitive: "null" },
10856
+ reactive: false,
10857
+ slotId: null,
10858
+ loc,
10859
+ origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
10860
+ };
10861
+ let result = info.fallback ? transformNode(info.fallback, ctx) ?? nullExpr : nullExpr;
10862
+ for (let i = info.branches.length - 1;i >= 0; i--) {
10863
+ const branch = info.branches[i];
10864
+ const caseConds = info.switchDiscriminant ? [branch.condition, ...branch.extraCaseConditions ?? []] : [branch.condition];
10865
+ let conditionText;
10866
+ if (info.switchDiscriminant) {
10867
+ const discText = getText(info.switchDiscriminant);
10868
+ conditionText = caseConds.map((c) => `(${discText}) === (${getText(c)})`).join(" || ");
10869
+ } else {
10870
+ conditionText = getText(branch.condition);
10871
+ }
10872
+ const env = makeBindingEnv(ctx);
10873
+ const caseFreeRefs = caseConds.flatMap((c) => resolveFreeRefs(c, env));
10874
+ const discFreeRefs = info.switchDiscriminant ? resolveFreeRefs(info.switchDiscriminant, env) : [];
10875
+ const conditionOrigin = {
10876
+ phase: "tick",
10877
+ scope: "template",
10878
+ effect: "pure",
10879
+ freeRefs: [...discFreeRefs, ...caseFreeRefs]
10880
+ };
10881
+ const reactive = isReactiveExpression(conditionText, ctx, branch.condition) || isReactiveOrigin(conditionOrigin);
10882
+ const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx);
10883
+ const callsReactive = caseConds.some((c) => exprCallsReactiveGetters(c, ctx)) || (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx) : false);
10884
+ const hasCalls = caseConds.some((c) => exprHasFunctionCalls(c)) || (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false);
10885
+ const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls;
10886
+ const slotId = needsSlot ? generateSlotId(ctx) : null;
10887
+ const whenTrue = branch.jsxReturn ? transformNode(branch.jsxReturn, ctx) ?? nullExpr : nullExpr;
10888
+ let templateCondition;
10889
+ if (info.switchDiscriminant) {
10890
+ const discRewritten = rewriteBarePropRefs2(getText(info.switchDiscriminant), info.switchDiscriminant, ctx);
10891
+ const discPart = discRewritten ?? getText(info.switchDiscriminant);
10892
+ templateCondition = caseConds.map((c) => {
10893
+ const casePart = rewriteBarePropRefs2(getText(c), c, ctx) ?? getText(c);
10894
+ return `(${discPart}) === (${casePart})`;
10895
+ }).join(" || ");
10896
+ } else {
10897
+ templateCondition = rewriteBarePropRefs2(conditionText, branch.condition, ctx);
10898
+ }
10899
+ const conditional = {
10900
+ type: "conditional",
10901
+ condition: conditionText,
10902
+ templateCondition,
10903
+ conditionType: null,
10904
+ reactive,
10905
+ whenTrue,
10906
+ whenFalse: result,
10907
+ slotId,
10908
+ callsReactiveGetters: callsReactive || undefined,
10909
+ hasFunctionCalls: hasCalls || undefined,
10910
+ loc,
10911
+ origin: conditionOrigin
10912
+ };
10913
+ result = conditional;
10914
+ }
10915
+ return result;
10916
+ }
10758
10917
  function transformConditional(node, ctx) {
10759
10918
  const condition = ctx.getJS(node.condition);
10760
10919
  const conditionOrigin = {
@@ -11605,9 +11764,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11605
11764
  let filterPredicate;
11606
11765
  let sortComparator;
11607
11766
  let chainOrder;
11608
- let mapPreamble;
11609
- let templateMapPreamble;
11610
- let typedMapPreamble;
11767
+ let preamble;
11611
11768
  let iterationShape;
11612
11769
  let objectIteration;
11613
11770
  const setArray = (node2) => {
@@ -11637,7 +11794,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11637
11794
  const innerFilter = isFilterCall(sortInfo.array);
11638
11795
  const sortExtraction = extractSortComparator(sortInfo.callback, sortInfo.method, ctx);
11639
11796
  if (isClientOnly || !sortExtraction.result) {
11640
- if (!isClientOnly && sortExtraction.unsupportedReason) {
11797
+ if (!isClientOnly && sortExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("sort") ?? false)) {
11641
11798
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(sortInfo.callback, ctx.sourceFile, ctx.filePath), {
11642
11799
  message: `Expression cannot be compiled to marked template: ${sortExtraction.unsupportedReason}`,
11643
11800
  suggestion: {
@@ -11652,7 +11809,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11652
11809
  chainOrder = "filter-sort";
11653
11810
  const filterExtraction = extractFilterPredicate(innerFilter.callback, ctx);
11654
11811
  if (isClientOnly || !filterExtraction.result) {
11655
- if (!isClientOnly && filterExtraction.unsupportedReason) {
11812
+ if (!isClientOnly && filterExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("filter") ?? false)) {
11656
11813
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(innerFilter.callback, ctx.sourceFile, ctx.filePath), {
11657
11814
  message: `Expression cannot be compiled to marked template: ${filterExtraction.unsupportedReason}`,
11658
11815
  suggestion: {
@@ -11675,7 +11832,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11675
11832
  const innerSort = isSortCall(filterInfo.array);
11676
11833
  const filterExtraction = extractFilterPredicate(filterInfo.callback, ctx);
11677
11834
  if (isClientOnly || !filterExtraction.result) {
11678
- if (!isClientOnly && filterExtraction.unsupportedReason) {
11835
+ if (!isClientOnly && filterExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("filter") ?? false)) {
11679
11836
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(filterInfo.callback, ctx.sourceFile, ctx.filePath), {
11680
11837
  message: `Expression cannot be compiled to marked template: ${filterExtraction.unsupportedReason}`,
11681
11838
  suggestion: {
@@ -11690,7 +11847,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11690
11847
  chainOrder = "sort-filter";
11691
11848
  const sortExtraction = extractSortComparator(innerSort.callback, innerSort.method, ctx);
11692
11849
  if (isClientOnly || !sortExtraction.result) {
11693
- if (!isClientOnly && sortExtraction.unsupportedReason) {
11850
+ if (!isClientOnly && sortExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("sort") ?? false)) {
11694
11851
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(innerSort.callback, ctx.sourceFile, ctx.filePath), {
11695
11852
  message: `Expression cannot be compiled to marked template: ${sortExtraction.unsupportedReason}`,
11696
11853
  suggestion: {
@@ -11805,7 +11962,24 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11805
11962
  } else if (method === "flatMap" && ts11.isArrayLiteralExpression(body)) {
11806
11963
  children = transformArrayLiteralChildren(body, ctx);
11807
11964
  } else if (ts11.isBlock(body)) {
11808
- const returnStmt = body.statements.find((s) => ts11.isReturnStatement(s) && s.expression != null);
11965
+ const multiReturn = method !== "flatMap" ? extractMultiReturnJsxBranches(body, true) : null;
11966
+ if (multiReturn && multiReturn.branches.length > 0) {
11967
+ const loc = getSourceLocation(body, ctx.sourceFile, ctx.filePath);
11968
+ children = [foldMultiReturnBranches(multiReturn, ctx, loc, ctx.getJS)];
11969
+ const pre = multiReturn.preamble ?? [];
11970
+ if (pre.length > 0) {
11971
+ preamble = preambleFromValueStatements(pre, ctx);
11972
+ if (!isClientOnly && !(ctx.analyzer.acceptsCallbackBody?.("map") ?? false)) {
11973
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, loc, {
11974
+ message: "A .map() callback body with a `const`/`let` preamble before its " + "branches cannot be lowered to a template: the loop-local binding " + "cannot be carried into a conditional branch on this backend.",
11975
+ suggestion: {
11976
+ message: "Add /* @client */ to evaluate this expression on the client only"
11977
+ }
11978
+ }));
11979
+ }
11980
+ }
11981
+ }
11982
+ const returnStmt = children.length === 0 ? body.statements.find((s) => ts11.isReturnStatement(s) && s.expression != null) : undefined;
11809
11983
  if (returnStmt && returnStmt.expression) {
11810
11984
  let returnExpr = returnStmt.expression;
11811
11985
  while (ts11.isParenthesizedExpression(returnExpr)) {
@@ -11817,32 +11991,53 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11817
11991
  children = [transformed];
11818
11992
  }
11819
11993
  }
11820
- const preambleStmts = [];
11821
- const templatePreambleStmts = [];
11822
- const typedPreambleStmts = [];
11823
- let hasTypeDiff = false;
11824
- let hasTemplateDiff = false;
11994
+ let jsxPreambleStmt;
11825
11995
  for (const stmt of body.statements) {
11826
11996
  if (stmt === returnStmt)
11827
11997
  break;
11828
- const js = ctx.getJS(stmt);
11829
- const tjs = ctx.getTemplateJS(stmt);
11830
- const ts12 = stmt.getText(ctx.sourceFile);
11831
- preambleStmts.push(js.endsWith(";") ? js : js + ";");
11832
- templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
11833
- typedPreambleStmts.push(ts12.endsWith(";") ? ts12 : ts12 + ";");
11834
- if (js !== ts12)
11835
- hasTypeDiff = true;
11836
- if (js !== tjs)
11837
- hasTemplateDiff = true;
11998
+ if (containsJsxInExpression(stmt)) {
11999
+ jsxPreambleStmt = stmt;
12000
+ break;
12001
+ }
11838
12002
  }
11839
- if (preambleStmts.length > 0) {
11840
- mapPreamble = preambleStmts.join(" ");
11841
- if (hasTemplateDiff) {
11842
- templateMapPreamble = templatePreambleStmts.join(" ");
12003
+ if (jsxPreambleStmt) {
12004
+ const jsRuntime = isClientOnly || (ctx.analyzer.acceptsCallbackBody?.("map") ?? false);
12005
+ if (children.length === 0) {
12006
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(jsxPreambleStmt, ctx.sourceFile, ctx.filePath), {
12007
+ message: "A .map() callback that builds JSX in a statement before its " + "`return` must return a single JSX element that embeds the " + "built array — this return shape has no element root to host it.",
12008
+ suggestion: {
12009
+ message: "Wrap the result in one element root, e.g. `return <tr key={item.id}>{out}</tr>`."
12010
+ }
12011
+ }));
12012
+ } else if (!jsRuntime) {
12013
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(jsxPreambleStmt, ctx.sourceFile, ctx.filePath), {
12014
+ message: "A .map() callback that builds JSX in a statement before its " + "`return` (for example pushing elements into an array in a loop) " + "cannot be lowered to a template on this backend.",
12015
+ suggestion: {
12016
+ message: "Add /* @client */ to render this loop on the client only"
12017
+ }
12018
+ }));
12019
+ } else {
12020
+ const collected = buildPreambleSegments(body.statements, returnStmt, ctx);
12021
+ if (collected.refusalNode) {
12022
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(collected.refusalNode, ctx.sourceFile, ctx.filePath), {
12023
+ message: "A JSX element built in a .map() callback preamble cannot carry " + "event handlers, components, nested loops, or reactive expressions, " + "and cannot sit inside a template literal — the verbatim render " + "path builds it once, with no reactive wiring.",
12024
+ suggestion: {
12025
+ message: "Return the element directly (not through a preamble variable), " + "or add /* @client */ to render the loop on the client only."
12026
+ }
12027
+ }));
12028
+ } else {
12029
+ preamble = collected.preamble;
12030
+ }
11843
12031
  }
11844
- if (hasTypeDiff) {
11845
- typedMapPreamble = typedPreambleStmts.join(" ");
12032
+ } else {
12033
+ const valueStmts = [];
12034
+ for (const stmt of body.statements) {
12035
+ if (stmt === returnStmt)
12036
+ break;
12037
+ valueStmts.push(stmt);
12038
+ }
12039
+ if (valueStmts.length > 0) {
12040
+ preamble = preambleFromValueStatements(valueStmts, ctx);
11846
12041
  }
11847
12042
  }
11848
12043
  }
@@ -11871,6 +12066,31 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11871
12066
  const itemConditional = children.length > 0 ? loopBodyItemConditional(children) : null;
11872
12067
  const bodyIsItemConditional = itemConditional !== null;
11873
12068
  const key = bodyIsItemConditional ? extractItemConditionalKey(itemConditional) : children.length > 0 ? extractLoopKey(children[0]) : null;
12069
+ const declaredNameSet = preamble && preamble.declaredNames.length > 0 ? new Set(preamble.declaredNames) : undefined;
12070
+ if (key && declaredNameSet) {
12071
+ const keyRefs = extractFreeIdentifiersFromText(key);
12072
+ const usesLocal = [...keyRefs].some((r) => declaredNameSet.has(r));
12073
+ if (usesLocal) {
12074
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(node, ctx.sourceFile, ctx.filePath), {
12075
+ message: "A .map() loop key must be derivable from the loop item — it is " + "evaluated before the callback body runs, so it cannot reference a " + "value computed in the callback preamble.",
12076
+ suggestion: {
12077
+ message: "Derive the key directly from the loop item (e.g. key={item.id})."
12078
+ }
12079
+ }));
12080
+ }
12081
+ }
12082
+ if (preamble && preamble.builderNames.length > 0) {
12083
+ flagArrayChildExpressions(children, new Set(preamble.builderNames));
12084
+ }
12085
+ if (preamble && preamble.builderNames.length > 0 && children.length === 1 && children[0].type === "component") {
12086
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(node, ctx.sourceFile, ctx.filePath), {
12087
+ message: "A .map() callback that builds JSX in its preamble cannot return a " + "component: the built elements would reach the component as raw HTML " + "strings on the client but as JSX elements at SSR (a silent divergence).",
12088
+ suggestion: {
12089
+ message: "Return a plain element root that embeds the array, or move the " + "building logic inside the component."
12090
+ }
12091
+ }));
12092
+ preamble = undefined;
12093
+ }
11874
12094
  let childComponent;
11875
12095
  if (children.length === 1 && children[0].type === "component") {
11876
12096
  const comp = children[0];
@@ -11919,11 +12139,9 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
11919
12139
  objectIteration,
11920
12140
  depth,
11921
12141
  clientOnly: isClientOnly || undefined,
11922
- mapPreamble,
11923
- templateMapPreamble,
12142
+ preamble,
11924
12143
  paramType,
11925
12144
  indexType,
11926
- typedMapPreamble,
11927
12145
  paramBindings,
11928
12146
  arrayFreeIdentifiers: extractFreeIdentifiersFromNode(arrayExpr),
11929
12147
  flatMapCallback,
@@ -11959,47 +12177,206 @@ function containsJsx(node) {
11959
12177
  function buildFlatMapCallback(callback, body, ctx) {
11960
12178
  if (!containsJsx(body))
11961
12179
  return;
11962
- const fragments = [];
11963
- const sourceText = ctx.sourceFile.text;
11964
- const bodyStart = body.getStart(ctx.sourceFile);
11965
- const bodyEnd = body.getEnd();
11966
- const bodyText = sourceText.slice(bodyStart, bodyEnd);
11967
- const jsxNodes = [];
11968
- function collectJsx(n) {
12180
+ const leafSpans = [];
12181
+ const leafIrs = [];
12182
+ let refusalNode;
12183
+ const collectJsx = (n, underTemplate) => {
11969
12184
  if (ts11.isJsxElement(n) || ts11.isJsxSelfClosingElement(n) || ts11.isJsxFragment(n)) {
11970
- jsxNodes.push({
11971
- node: n,
11972
- start: n.getStart(ctx.sourceFile) - bodyStart,
11973
- end: n.getEnd() - bodyStart
11974
- });
12185
+ if (underTemplate)
12186
+ refusalNode ??= n;
12187
+ leafSpans.push({ start: n.getStart(ctx.sourceFile), end: n.getEnd() });
12188
+ const ir = transformNode(n, ctx);
12189
+ leafIrs.push(ir ?? { type: "text", value: "", loc: getSourceLocation(n, ctx.sourceFile, ctx.filePath) });
11975
12190
  return;
11976
12191
  }
11977
- n.forEachChild(collectJsx);
11978
- }
11979
- collectJsx(body);
11980
- if (jsxNodes.length === 0)
12192
+ const inTemplate = underTemplate || ts11.isTemplateExpression(n) || ts11.isTaggedTemplateExpression(n);
12193
+ n.forEachChild((c) => collectJsx(c, inTemplate));
12194
+ };
12195
+ collectJsx(body, false);
12196
+ if (leafSpans.length === 0)
12197
+ return;
12198
+ if (refusalNode) {
12199
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(refusalNode, ctx.sourceFile, ctx.filePath), {
12200
+ message: "A JSX element inside a template literal in a .flatMap() callback " + "body cannot be compiled.",
12201
+ suggestion: { message: "Build the element outside the template literal." }
12202
+ }));
11981
12203
  return;
11982
- let compiledBody = "";
11983
- let lastEnd = 0;
11984
- for (let i = 0;i < jsxNodes.length; i++) {
11985
- const { node, start, end } = jsxNodes[i];
11986
- const placeholder = `__BF_JSX_${i}__`;
11987
- compiledBody += bodyText.slice(lastEnd, start) + placeholder;
11988
- lastEnd = end;
11989
- const ir = transformNode(node, ctx);
11990
- fragments.push({
11991
- placeholder,
11992
- ir: ir ?? { type: "text", value: "", loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath) }
11993
- });
11994
12204
  }
11995
- compiledBody += bodyText.slice(lastEnd);
12205
+ const pieces = reconstructAsSegments(body, ctx.sourceFile, ctx.analyzer.typeExcludeRanges, leafSpans);
12206
+ const segments = pieces.map((piece) => {
12207
+ if ("marker" in piece)
12208
+ return { kind: "jsx", ir: leafIrs[piece.marker] };
12209
+ const tpl = rewriteBarePropRefs2(piece.js, body, ctx);
12210
+ return tpl !== undefined && tpl !== piece.js ? { kind: "js", text: piece.js, templateText: tpl } : { kind: "js", text: piece.js };
12211
+ });
11996
12212
  const paramsText = callback.parameters.map((p) => p.getText(ctx.sourceFile)).join(", ");
11997
12213
  return {
11998
12214
  params: `(${paramsText})`,
11999
- body: compiledBody,
12000
- templateBody: compiledBody,
12001
- rawBody: bodyText,
12002
- fragments
12215
+ segments,
12216
+ rawBody: tsxSourceText(body.getText(ctx.sourceFile))
12217
+ };
12218
+ }
12219
+ function preambleFragmentNeedsWiring(ir) {
12220
+ switch (ir.type) {
12221
+ case "component":
12222
+ case "loop":
12223
+ return true;
12224
+ case "element":
12225
+ if (ir.events.length > 0)
12226
+ return true;
12227
+ if (ir.attrs.some((a) => a.name.startsWith("...")))
12228
+ return true;
12229
+ return ir.children.some(preambleFragmentNeedsWiring);
12230
+ case "expression":
12231
+ return ir.reactive === true;
12232
+ case "conditional":
12233
+ return preambleFragmentNeedsWiring(ir.whenTrue) || (ir.whenFalse ? preambleFragmentNeedsWiring(ir.whenFalse) : false);
12234
+ case "fragment":
12235
+ return ir.children.some(preambleFragmentNeedsWiring);
12236
+ default:
12237
+ return false;
12238
+ }
12239
+ }
12240
+ function flagArrayChildExpressions(nodes, declared) {
12241
+ for (const node of nodes) {
12242
+ switch (node.type) {
12243
+ case "expression": {
12244
+ const name = node.expr.trim();
12245
+ const refs = extractFreeIdentifiersFromText(node.expr);
12246
+ if (refs.size === 1 && refs.has(name) && declared.has(name)) {
12247
+ node.joinArrayChild = true;
12248
+ }
12249
+ break;
12250
+ }
12251
+ case "element":
12252
+ case "fragment":
12253
+ flagArrayChildExpressions(node.children, declared);
12254
+ break;
12255
+ case "conditional":
12256
+ flagArrayChildExpressions([node.whenTrue, ...node.whenFalse ? [node.whenFalse] : []], declared);
12257
+ break;
12258
+ }
12259
+ }
12260
+ }
12261
+ function collectBindingNames(name, out) {
12262
+ if (ts11.isIdentifier(name)) {
12263
+ out.add(name.text);
12264
+ return;
12265
+ }
12266
+ for (const el of name.elements) {
12267
+ if (ts11.isBindingElement(el))
12268
+ collectBindingNames(el.name, out);
12269
+ }
12270
+ }
12271
+ function collectPreambleDeclaredNames(stmt, out) {
12272
+ if (ts11.isVariableStatement(stmt)) {
12273
+ for (const decl of stmt.declarationList.declarations) {
12274
+ collectBindingNames(decl.name, out);
12275
+ }
12276
+ } else if (ts11.isFunctionDeclaration(stmt) && stmt.name) {
12277
+ out.add(stmt.name.text);
12278
+ }
12279
+ }
12280
+ function preambleFromValueStatements(statements, ctx) {
12281
+ const segments = [];
12282
+ const typedParts = [];
12283
+ const declared = new Set;
12284
+ for (const stmt of statements) {
12285
+ collectPreambleDeclaredNames(stmt, declared);
12286
+ const js0 = ctx.getJS(stmt);
12287
+ const tjs0 = ctx.getTemplateJS(stmt);
12288
+ const raw0 = stmt.getText(ctx.sourceFile);
12289
+ const js = (js0.endsWith(";") ? js0 : js0 + ";") + " ";
12290
+ const tjs = (tjs0.endsWith(";") ? tjs0 : tjs0 + ";") + " ";
12291
+ typedParts.push(raw0.endsWith(";") ? raw0 : raw0 + ";");
12292
+ segments.push(tjs !== js ? { kind: "js", text: js, templateText: tjs } : { kind: "js", text: js });
12293
+ }
12294
+ return {
12295
+ segments: trimPreambleSegments(segments),
12296
+ ssrText: tsxSourceText(typedParts.join(" ")),
12297
+ declaredNames: [...declared],
12298
+ builderNames: []
12299
+ };
12300
+ }
12301
+ function trimPreambleSegments(segments) {
12302
+ const last = segments[segments.length - 1];
12303
+ if (last?.kind === "js") {
12304
+ const text = last.text.trimEnd();
12305
+ const templateText = last.templateText?.trimEnd();
12306
+ segments[segments.length - 1] = templateText !== undefined ? { kind: "js", text, templateText } : { kind: "js", text };
12307
+ }
12308
+ return segments;
12309
+ }
12310
+ function buildPreambleSegments(statements, returnStmt, ctx) {
12311
+ const segments = [];
12312
+ const typedParts = [];
12313
+ const declared = new Set;
12314
+ const builders = new Set;
12315
+ let refusalNode;
12316
+ const recordBuilderTarget = (leaf, stmt) => {
12317
+ for (let n = leaf.parent;n && n !== stmt.parent; n = n.parent) {
12318
+ if (ts11.isCallExpression(n) && ts11.isPropertyAccessExpression(n.expression) && (n.expression.name.text === "push" || n.expression.name.text === "unshift") && ts11.isIdentifier(n.expression.expression)) {
12319
+ builders.add(n.expression.expression.text);
12320
+ return;
12321
+ }
12322
+ if (ts11.isVariableDeclaration(n) && ts11.isIdentifier(n.name)) {
12323
+ builders.add(n.name.text);
12324
+ return;
12325
+ }
12326
+ }
12327
+ };
12328
+ for (const stmt of statements) {
12329
+ if (stmt === returnStmt)
12330
+ break;
12331
+ collectPreambleDeclaredNames(stmt, declared);
12332
+ const leafSpans = [];
12333
+ const leafIrs = [];
12334
+ const collect = (n, underTemplate) => {
12335
+ if (ts11.isJsxElement(n) || ts11.isJsxSelfClosingElement(n) || ts11.isJsxFragment(n)) {
12336
+ if (underTemplate)
12337
+ refusalNode ??= n;
12338
+ recordBuilderTarget(n, stmt);
12339
+ leafSpans.push({ start: n.getStart(ctx.sourceFile), end: n.getEnd() });
12340
+ const ir = transformNode(n, ctx);
12341
+ if (ir && preambleFragmentNeedsWiring(ir))
12342
+ refusalNode ??= n;
12343
+ leafIrs.push(ir ?? { type: "text", value: "", loc: getSourceLocation(n, ctx.sourceFile, ctx.filePath) });
12344
+ return;
12345
+ }
12346
+ const inTemplate = underTemplate || ts11.isTemplateExpression(n) || ts11.isTaggedTemplateExpression(n);
12347
+ n.forEachChild((c) => collect(c, inTemplate));
12348
+ };
12349
+ collect(stmt, false);
12350
+ const raw0 = stmt.getText(ctx.sourceFile);
12351
+ typedParts.push(raw0.endsWith(";") ? raw0 : raw0 + ";");
12352
+ if (leafSpans.length === 0) {
12353
+ const js0 = ctx.getJS(stmt);
12354
+ const tjs0 = ctx.getTemplateJS(stmt);
12355
+ const js = (js0.endsWith(";") ? js0 : js0 + ";") + " ";
12356
+ const tjs = (tjs0.endsWith(";") ? tjs0 : tjs0 + ";") + " ";
12357
+ segments.push(tjs !== js ? { kind: "js", text: js, templateText: tjs } : { kind: "js", text: js });
12358
+ continue;
12359
+ }
12360
+ const pieces = reconstructAsSegments(stmt, ctx.sourceFile, ctx.analyzer.typeExcludeRanges, leafSpans);
12361
+ for (const piece of pieces) {
12362
+ if ("marker" in piece) {
12363
+ segments.push({ kind: "jsx", ir: leafIrs[piece.marker] });
12364
+ } else {
12365
+ const tpl = rewriteBarePropRefs2(piece.js, stmt, ctx);
12366
+ segments.push(tpl !== undefined && tpl !== piece.js ? { kind: "js", text: piece.js, templateText: tpl } : { kind: "js", text: piece.js });
12367
+ }
12368
+ }
12369
+ const sep2 = raw0.endsWith(";") ? " " : "; ";
12370
+ segments.push({ kind: "js", text: sep2 });
12371
+ }
12372
+ return {
12373
+ preamble: {
12374
+ segments: trimPreambleSegments(segments),
12375
+ ssrText: tsxSourceText(typedParts.join(" ")),
12376
+ declaredNames: [...declared],
12377
+ builderNames: [...builders]
12378
+ },
12379
+ refusalNode
12003
12380
  };
12004
12381
  }
12005
12382
  function collectNestedComponents(nodes) {
@@ -13642,7 +14019,7 @@ function collectInnerLoops(nodes, siblingOffsets, outerLoopParam, ctx, options)
13642
14019
  objectIteration: n.objectIteration,
13643
14020
  containerSlotId: scope.parentSlotId,
13644
14021
  template,
13645
- mapPreamble: n.mapPreamble,
14022
+ preamble: n.preamble,
13646
14023
  refsOuterParam: refsOuter,
13647
14024
  childComponents,
13648
14025
  insideConditional: !flat && scope.insideCond ? true : undefined,
@@ -13889,7 +14266,7 @@ function collectElements(node, ctx, siblingOffsets, insideConditional = false) {
13889
14266
  raw: l.sortComparator.raw
13890
14267
  } : undefined,
13891
14268
  chainOrder: l.chainOrder,
13892
- mapPreamble: l.mapPreamble
14269
+ preamble: l.preamble
13893
14270
  });
13894
14271
  },
13895
14272
  component: ({ node: c, descend, descendJsxChildren }) => {
@@ -14070,7 +14447,7 @@ function collectBranchLoops(node, ctx, siblingOffsets) {
14070
14447
  objectIteration: n.objectIteration,
14071
14448
  template: childTemplate,
14072
14449
  containerSlotId: containerSlot,
14073
- mapPreamble: n.mapPreamble ?? null,
14450
+ preamble: n.preamble,
14074
14451
  nestedComponents: useElementReconciliation ? n.nestedComponents : undefined,
14075
14452
  bindings: branchBindings,
14076
14453
  innerLoops: useElementReconciliation ? innerLoopsCollected : undefined,
@@ -14346,8 +14723,8 @@ function buildReferencesGraph(ctx, irRoot) {
14346
14723
  addExprEdges(ROOT_SOURCE, elem.filterPredicate.raw, "template-closure");
14347
14724
  if (elem.sortComparator)
14348
14725
  addExprEdges(ROOT_SOURCE, elem.sortComparator.raw, "template-closure");
14349
- if (elem.mapPreamble)
14350
- addExprEdges(ROOT_SOURCE, elem.mapPreamble, "template-closure");
14726
+ if (elem.preamble)
14727
+ addExprEdges(ROOT_SOURCE, preambleAnalysisText(elem.preamble), "template-closure");
14351
14728
  for (const attr of elem.bindings.reactiveAttrs) {
14352
14729
  addExprEdges(ROOT_SOURCE, attr.expression, "template-closure");
14353
14730
  }
@@ -14457,8 +14834,8 @@ function buildReferencesGraph(ctx, irRoot) {
14457
14834
  addExprEdges(ROOT_SOURCE, l.filterPredicate.raw, "template-closure");
14458
14835
  if (l.sortComparator)
14459
14836
  addExprEdges(ROOT_SOURCE, l.sortComparator.raw, "template-closure");
14460
- if (l.mapPreamble)
14461
- addExprEdges(ROOT_SOURCE, l.mapPreamble, "template-closure");
14837
+ if (l.preamble)
14838
+ addExprEdges(ROOT_SOURCE, preambleAnalysisText(l.preamble), "template-closure");
14462
14839
  descend();
14463
14840
  if (l.childComponent)
14464
14841
  walkChildComponent(l.childComponent);
@@ -16567,7 +16944,7 @@ function nestedLoopReferencesIndex(inner, comps, events) {
16567
16944
  if (exprRefs(a.expression, a.freeIdentifiers))
16568
16945
  return true;
16569
16946
  }
16570
- if (inner.mapPreamble && extractFreeIdentifiersFromStatementText(inner.mapPreamble).has(index))
16947
+ if (inner.preamble && extractFreeIdentifiersFromStatementText(preambleAnalysisText(inner.preamble)).has(index))
16571
16948
  return true;
16572
16949
  if (inner.template && extractFreeIdentifiersFromTemplateText(inner.template).has(index))
16573
16950
  return true;
@@ -16722,6 +17099,11 @@ function emitComponentAndEventSetup(ls, indent, elVar, comps, events, loopParam,
16722
17099
  }
16723
17100
 
16724
17101
  // src/ir-to-client-js/plan/build-static-array-child-init.ts
17102
+ function staticPreludeStatements(preamble) {
17103
+ return preamble ? [renderPreamble(preamble, {
17104
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true)
17105
+ })] : [];
17106
+ }
16725
17107
  function buildStaticArrayChildInitsPlan(ctx) {
16726
17108
  const plans = [];
16727
17109
  for (const elem of ctx.loopElements) {
@@ -16759,7 +17141,7 @@ function buildSingleCompPlan(elem, childComponent) {
16759
17141
  arrayExpr: elem.array,
16760
17142
  param: elem.param,
16761
17143
  indexParam: elem.index || "__idx",
16762
- outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
17144
+ outerPreludeStatements: staticPreludeStatements(elem.preamble),
16763
17145
  propsExpr: buildStaticPropsExpr(props)
16764
17146
  };
16765
17147
  }
@@ -16774,7 +17156,7 @@ function buildOuterNestedPlan(elem, comp) {
16774
17156
  param: elem.param,
16775
17157
  indexParam,
16776
17158
  offsetExpr: buildLoopChildIndexExpr(indexParam, elem.offset),
16777
- outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
17159
+ outerPreludeStatements: staticPreludeStatements(elem.preamble),
16778
17160
  propsExpr: buildStaticPropsExpr(comp.props)
16779
17161
  };
16780
17162
  }
@@ -16793,13 +17175,13 @@ function buildInnerLoopNestedPlan(elem, innerLoop, innerComps) {
16793
17175
  outerParam: elem.param,
16794
17176
  outerIndexParam,
16795
17177
  outerOffsetExpr: buildLoopChildIndexExpr(outerIndexParam, elem.offset),
16796
- outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
17178
+ outerPreludeStatements: staticPreludeStatements(elem.preamble),
16797
17179
  innerContainerSlotId: innerLoop.containerSlotId ?? null,
16798
17180
  innerArrayExpr: innerLoop.array,
16799
17181
  innerParam: innerLoop.param,
16800
17182
  innerIndexParam,
16801
17183
  innerOffsetExpr: buildLoopChildIndexExpr(innerIndexParam, innerLoop.offset),
16802
- innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
17184
+ innerPreludeStatements: staticPreludeStatements(innerLoop.preamble),
16803
17185
  depth: innerLoop.depth,
16804
17186
  comps
16805
17187
  };
@@ -16816,11 +17198,11 @@ function buildComponentRootedInnerLoopPlan(elem, innerLoop, innerComps) {
16816
17198
  outerArrayExpr: elem.array,
16817
17199
  outerParam: elem.param,
16818
17200
  outerIndexParam: elem.index,
16819
- outerPreludeStatements: elem.mapPreamble ? [elem.mapPreamble] : [],
17201
+ outerPreludeStatements: staticPreludeStatements(elem.preamble),
16820
17202
  innerArrayExpr: innerLoop.array,
16821
17203
  innerParam: innerLoop.param,
16822
17204
  innerIndexParam: innerLoop.index,
16823
- innerPreludeStatements: innerLoop.mapPreamble ? [innerLoop.mapPreamble] : [],
17205
+ innerPreludeStatements: staticPreludeStatements(innerLoop.preamble),
16824
17206
  depth: innerLoop.depth,
16825
17207
  comps
16826
17208
  };
@@ -17389,7 +17771,7 @@ function buildInnerLoopsPlan(args) {
17389
17771
  const containerExpr = inner.containerSlotId ? `qsa(${parentElVar}, '[bf="${inner.containerSlotId}"]')` : parentElVar;
17390
17772
  const refsParent = !!outerLoopParam && (inner.arrayFreeIdentifiers?.has(outerLoopParam) ?? false);
17391
17773
  const useReactive = refsParent && !!inner.template;
17392
- const emit = useReactive ? buildReactiveEmit(inner, level, wrapOuter, uidSuffix) : buildStaticEmit(inner, level, uidSuffix);
17774
+ const emit = useReactive ? buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings) : buildStaticEmit(inner, level, uidSuffix);
17393
17775
  const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array;
17394
17776
  const childLevelsPlan = childLevels.length > 0 ? buildInnerLoopsPlan({
17395
17777
  levels: childLevels,
@@ -17415,7 +17797,7 @@ function buildInnerLoopsPlan(args) {
17415
17797
  }
17416
17798
  return plan;
17417
17799
  }
17418
- function buildReactiveEmit(inner, level, wrapOuter, uidSuffix) {
17800
+ function buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings) {
17419
17801
  const wrapInner = (expr) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings);
17420
17802
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings);
17421
17803
  const wrappedKey = inner.key ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings) : null;
@@ -17467,8 +17849,16 @@ function buildReactiveEmit(inner, level, wrapOuter, uidSuffix) {
17467
17849
  preludeStatements.push(indexAlias);
17468
17850
  if (paramUnwrap)
17469
17851
  preludeStatements.push(paramUnwrap);
17470
- if (inner.mapPreamble)
17471
- preludeStatements.push(wrapInner(wrapOuter(inner.mapPreamble)));
17852
+ if (inner.preamble) {
17853
+ const leafLoopParams = outerLoopParam ? [
17854
+ { param: outerLoopParam, bindings: outerLoopParamBindings },
17855
+ { param: inner.param, bindings: inner.paramBindings }
17856
+ ] : [{ param: inner.param, bindings: inner.paramBindings }];
17857
+ preludeStatements.push(renderPreamble(inner.preamble, {
17858
+ transformJs: (t) => wrapInner(wrapOuter(t)),
17859
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined, true)
17860
+ }));
17861
+ }
17472
17862
  const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings);
17473
17863
  return {
17474
17864
  mode: "reactive",
@@ -17490,8 +17880,11 @@ function buildStaticEmit(inner, level, uidSuffix) {
17490
17880
  const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events);
17491
17881
  if (indexAlias)
17492
17882
  preludeStatements.push(indexAlias);
17493
- if (inner.mapPreamble)
17494
- preludeStatements.push(inner.mapPreamble);
17883
+ if (inner.preamble) {
17884
+ preludeStatements.push(renderPreamble(inner.preamble, {
17885
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true)
17886
+ }));
17887
+ }
17495
17888
  return {
17496
17889
  mode: "static",
17497
17890
  rawKey: inner.key ?? null,
@@ -17511,6 +17904,7 @@ function buildTopLevelCompositePlan(elem, profileComponentName) {
17511
17904
  const outerCompsByDepth = nestedComps.filter((c) => !c.loopDepth || c.loopDepth === 0);
17512
17905
  return {
17513
17906
  kind: "composite",
17907
+ rowConstruction: "string-template",
17514
17908
  containerVar: `_${varSlotId(elem.slotId)}`,
17515
17909
  markerId: elem.markerId,
17516
17910
  arrayExpr: buildChainedArrayExpr(elem),
@@ -17518,7 +17912,10 @@ function buildTopLevelCompositePlan(elem, profileComponentName) {
17518
17912
  paramHead,
17519
17913
  paramUnwrap,
17520
17914
  indexParam: elem.index || "__idx",
17521
- mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : "",
17915
+ mapPreambleWrapped: elem.preamble ? renderPreamble(elem.preamble, {
17916
+ transformJs: wrap,
17917
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true)
17918
+ }) : "",
17522
17919
  template: elem.template,
17523
17920
  outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
17524
17921
  outerEvents: elem.bindings.events.filter((ev) => ev.nestedLoops.length === 0),
@@ -17557,6 +17954,7 @@ function buildBranchCompositePlan(loop, cv, profileComponentName) {
17557
17954
  const outerCompsByDepth = nestedComps.filter((c) => !c.loopDepth || c.loopDepth === 0);
17558
17955
  return {
17559
17956
  kind: "composite",
17957
+ rowConstruction: "string-template",
17560
17958
  containerVar: `__loop_${cv}`,
17561
17959
  markerId: loop.markerId,
17562
17960
  arrayExpr: buildChainedArrayExpr(loop),
@@ -17564,7 +17962,10 @@ function buildBranchCompositePlan(loop, cv, profileComponentName) {
17564
17962
  paramHead,
17565
17963
  paramUnwrap,
17566
17964
  indexParam: loop.index || "__idx",
17567
- mapPreambleWrapped: loop.mapPreamble ? wrap(loop.mapPreamble) : "",
17965
+ mapPreambleWrapped: loop.preamble ? renderPreamble(loop.preamble, {
17966
+ transformJs: wrap,
17967
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true)
17968
+ }) : "",
17568
17969
  template: loop.template,
17569
17970
  outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
17570
17971
  outerEvents: childEvents.filter((ev) => ev.nestedLoops.length === 0),
@@ -17627,7 +18028,9 @@ function buildDynamicLoopDelegationPlan(elem, profileComponentName) {
17627
18028
  paramBindings: elem.paramBindings,
17628
18029
  key: elem.key,
17629
18030
  index: elem.index,
17630
- mapPreamble: elem.mapPreamble ?? null
18031
+ mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
18032
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true)
18033
+ }) : null
17631
18034
  })
17632
18035
  };
17633
18036
  }
@@ -17643,7 +18046,9 @@ function buildBranchLoopDelegationPlan(loop, cv, profileComponentName) {
17643
18046
  paramBindings: loop.paramBindings,
17644
18047
  key: loop.key,
17645
18048
  index: loop.index,
17646
- mapPreamble: loop.mapPreamble ?? null
18049
+ mapPreamble: loop.preamble ? renderPreamble(loop.preamble, {
18050
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true)
18051
+ }) : null
17647
18052
  })
17648
18053
  };
17649
18054
  }
@@ -17657,7 +18062,9 @@ function buildStaticArrayDelegationPlan(elem, profileComponentName) {
17657
18062
  kind: "static-index",
17658
18063
  arrayExpr: buildChainedArrayExpr(elem),
17659
18064
  param: elem.param,
17660
- mapPreamble: elem.mapPreamble ?? null,
18065
+ mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
18066
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true)
18067
+ }) : null,
17661
18068
  offset: elem.offset ?? null,
17662
18069
  indexParam: elem.index ?? null
17663
18070
  }
@@ -17706,6 +18113,7 @@ function buildBranchLoopPlan(loop, profileComponentName) {
17706
18113
  const hasReactiveEffects = loop.bindings.reactiveAttrs.length > 0 || loop.bindings.reactiveTexts.length > 0 || loop.bindings.conditionals.length > 0;
17707
18114
  const plan = {
17708
18115
  kind: "plain",
18116
+ rowConstruction: "string-template",
17709
18117
  containerSlotId,
17710
18118
  containerVar,
17711
18119
  markerId: loop.markerId,
@@ -17714,7 +18122,10 @@ function buildBranchLoopPlan(loop, profileComponentName) {
17714
18122
  paramHead,
17715
18123
  paramUnwrap,
17716
18124
  indexParam: loop.index || "__idx",
17717
- mapPreambleWrapped: loop.mapPreamble ? wrapLoopParamAsAccessor(loop.mapPreamble, loop.param, loop.paramBindings) : "",
18125
+ mapPreambleWrapped: loop.preamble ? renderPreamble(loop.preamble, {
18126
+ transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
18127
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true)
18128
+ }) : "",
17718
18129
  template: loop.template,
17719
18130
  reactiveEffects: hasReactiveEffects ? buildReactiveEffectsPlan({
17720
18131
  attrs: loop.bindings.reactiveAttrs,
@@ -18804,7 +19215,7 @@ function stringifyEventDelegation(lines, plan) {
18804
19215
  for (const ev of evs) {
18805
19216
  const childVar = varSlotId(ev.childSlotId);
18806
19217
  lines.push(` const ${childVar}El = target.closest('[bf="${ev.childSlotId}"]')`);
18807
- lines.push(` if (${childVar}El) {`);
19218
+ lines.push(` if (${childVar}El && ${containerVar}.contains(${childVar}El)) {`);
18808
19219
  const handlerCall = withTurn(`(${ev.handler.trim()})(__bfEvt)`, profileComponentName, ev.childSlotId, ev.eventName);
18809
19220
  switch (itemLookup.kind) {
18810
19221
  case "keyed":
@@ -19130,6 +19541,7 @@ function buildComponentLoopPlan(elem, profileComponentName) {
19130
19541
  const hasChildConds = elem.bindings.conditionals.length > 0;
19131
19542
  return {
19132
19543
  kind: "component",
19544
+ rowConstruction: "dom-ops",
19133
19545
  containerVar: `_${varSlotId(elem.slotId)}`,
19134
19546
  markerId: elem.markerId,
19135
19547
  arrayExpr: buildChainedArrayExpr(elem),
@@ -19177,6 +19589,7 @@ function buildPlainLoopPlan(elem, profileComponentName) {
19177
19589
  const hasReactive2 = elem.bindings.reactiveAttrs.length > 0 || elem.bindings.reactiveTexts.length > 0 || elem.bindings.conditionals.length > 0;
19178
19590
  return {
19179
19591
  kind: "plain",
19592
+ rowConstruction: "string-template",
19180
19593
  containerVar: `_${varSlotId(elem.slotId)}`,
19181
19594
  markerId: elem.markerId,
19182
19595
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
@@ -19185,7 +19598,10 @@ function buildPlainLoopPlan(elem, profileComponentName) {
19185
19598
  paramHead,
19186
19599
  paramUnwrap,
19187
19600
  indexParam: elem.index || "__idx",
19188
- mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : "",
19601
+ mapPreambleWrapped: elem.preamble ? renderPreamble(elem.preamble, {
19602
+ transformJs: wrap,
19603
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true)
19604
+ }) : "",
19189
19605
  template: elem.template,
19190
19606
  skeletonTemplate: elem.skeletonTemplate,
19191
19607
  skeletonPaths: elem.skeletonPaths,
@@ -19212,6 +19628,7 @@ function buildStaticLoopPlan(elem, unsafeLocalNames, profileComponentName) {
19212
19628
  const childIndexExpr = buildLoopChildIndexExpr(indexParam, elem.offset);
19213
19629
  return {
19214
19630
  kind: "static",
19631
+ rowConstruction: "string-template",
19215
19632
  containerVar: `_${varSlotId(elem.slotId)}`,
19216
19633
  arrayExpr: elem.array,
19217
19634
  param: elem.param,
@@ -19233,7 +19650,9 @@ function buildStaticLoopMaterialize(elem, unsafeLocalNames) {
19233
19650
  return null;
19234
19651
  return {
19235
19652
  itemTemplate: elem.staticItemTemplate,
19236
- mapPreamble: elem.mapPreamble ?? "",
19653
+ mapPreamble: elem.preamble ? renderPreamble(elem.preamble, {
19654
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true)
19655
+ }) : "",
19237
19656
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false
19238
19657
  };
19239
19658
  }
@@ -19262,6 +19681,7 @@ function emitLoopUpdates(lines, ctx, unsafeLocalNames) {
19262
19681
  unsafeLocalNames,
19263
19682
  profileComponentName: ctx.profile ? ctx.componentName : undefined
19264
19683
  });
19684
+ internalInvariant(!(elem.preamble && elem.preamble.builderNames.length > 0 && plan.rowConstruction === "dom-ops"), `loop variant '${plan.kind}' declares dom-ops row construction but received a JSX-bearing preamble — add a Phase-1 refusal (or wire renderPreamble support) for this shape`);
19265
19685
  stringifyLoop(lines, plan);
19266
19686
  emitLoopEventDelegation(lines, elem, plan.kind, ctx.profile ? ctx.componentName : undefined);
19267
19687
  }
@@ -20192,19 +20612,19 @@ function isJsxLike(expr) {
20192
20612
  function collectArrowParamNames(arrow) {
20193
20613
  const names = new Set;
20194
20614
  for (const p of arrow.parameters)
20195
- collectBindingNames(p.name, names);
20615
+ collectBindingNames2(p.name, names);
20196
20616
  return names;
20197
20617
  }
20198
- function collectBindingNames(name, out) {
20618
+ function collectBindingNames2(name, out) {
20199
20619
  const push = Array.isArray(out) ? (n) => out.push(n) : (n) => out.add(n);
20200
20620
  if (ts16.isIdentifier(name)) {
20201
20621
  push(name.text);
20202
20622
  } else if (ts16.isObjectBindingPattern(name)) {
20203
- name.elements.forEach((el) => collectBindingNames(el.name, out));
20623
+ name.elements.forEach((el) => collectBindingNames2(el.name, out));
20204
20624
  } else if (ts16.isArrayBindingPattern(name)) {
20205
20625
  name.elements.forEach((el) => {
20206
20626
  if (!ts16.isOmittedExpression(el))
20207
- collectBindingNames(el.name, out);
20627
+ collectBindingNames2(el.name, out);
20208
20628
  });
20209
20629
  }
20210
20630
  }
@@ -20213,12 +20633,12 @@ function collectFreeIdentifiers(arrow) {
20213
20633
  const bound = [];
20214
20634
  for (const p of arrow.parameters) {
20215
20635
  const names = [];
20216
- collectBindingNames(p.name, names);
20636
+ collectBindingNames2(p.name, names);
20217
20637
  bound.push(...names);
20218
20638
  }
20219
20639
  function pushBindings(name) {
20220
20640
  const names = [];
20221
- collectBindingNames(name, names);
20641
+ collectBindingNames2(name, names);
20222
20642
  bound.push(...names);
20223
20643
  return names;
20224
20644
  }
@@ -20345,7 +20765,7 @@ function collectModuleScopeNames(sourceFile) {
20345
20765
  names.add(stmt.name.text);
20346
20766
  else if (ts16.isVariableStatement(stmt)) {
20347
20767
  for (const decl of stmt.declarationList.declarations)
20348
- collectBindingNames(decl.name, names);
20768
+ collectBindingNames2(decl.name, names);
20349
20769
  } else if (ts16.isImportDeclaration(stmt) && stmt.importClause) {
20350
20770
  const ic = stmt.importClause;
20351
20771
  if (ic.name)
@@ -21364,8 +21784,13 @@ function walkNode(node, meta, bindings, matchers, errors, seen) {
21364
21784
  for (const child of nested.children)
21365
21785
  walkNode(child, meta, loopBindings, matchers, errors, seen);
21366
21786
  }
21367
- for (const frag of node.flatMapCallback?.fragments ?? []) {
21368
- walkNode(frag.ir, meta, loopBindings, matchers, errors, seen);
21787
+ for (const seg of node.flatMapCallback?.segments ?? []) {
21788
+ if (seg.kind === "jsx")
21789
+ walkNode(seg.ir, meta, loopBindings, matchers, errors, seen);
21790
+ }
21791
+ for (const seg of node.preamble?.segments ?? []) {
21792
+ if (seg.kind === "jsx")
21793
+ walkNode(seg.ir, meta, loopBindings, matchers, errors, seen);
21369
21794
  }
21370
21795
  break;
21371
21796
  }
@@ -21427,7 +21852,7 @@ function compileMultipleComponents(source, filePath, componentNames, options) {
21427
21852
  const entries = [];
21428
21853
  const program = options.program ?? (needsTypeBasedDetection(source) ? createProgramForFile(source, filePath)?.program : undefined);
21429
21854
  for (const componentName of componentNames) {
21430
- const ctx = analyzeComponent(source, filePath, componentName, program);
21855
+ const ctx = analyzeComponent(source, filePath, componentName, program, adapter.acceptsCallbackBody);
21431
21856
  if (!ctx.jsxReturn) {
21432
21857
  errors.push(...ctx.errors);
21433
21858
  continue;
@@ -21736,7 +22161,7 @@ function compileJSX(source, filePath, options) {
21736
22161
  if (componentNames.length > 1) {
21737
22162
  return compileMultipleComponents(compileSource, filePath, componentNames, options);
21738
22163
  }
21739
- const ctx = analyzeComponent(compileSource, filePath, undefined, options.program);
22164
+ const ctx = analyzeComponent(compileSource, filePath, undefined, options.program, options.adapter.acceptsCallbackBody);
21740
22165
  if (!ctx.jsxReturn) {
21741
22166
  errors.push(...ctx.errors);
21742
22167
  const exportedModuleSignals = ctx.signals.filter((s2) => s2.isModule && s2.isExported);
@@ -21936,6 +22361,7 @@ class BaseAdapter {
21936
22361
  import { BF_SCOPE as BF_SCOPE4, BF_SLOT, BF_COND } from "@barefootjs/shared";
21937
22362
  class JsxAdapter extends BaseAdapter {
21938
22363
  componentName = "";
22364
+ acceptsCallbackBody = () => true;
21939
22365
  formatImportSpecifiers(specifiers) {
21940
22366
  const defaultSpec = specifiers.find((s) => s.isDefault);
21941
22367
  const namespaceSpec = specifiers.find((s) => s.isNamespace);
@@ -22281,6 +22707,10 @@ export default ${this.componentName}` : "";
22281
22707
  const indexParam = loop.index ? `, ${loop.index}` : "";
22282
22708
  const children = this.renderChildren(loop.children);
22283
22709
  const safeChildren = children.startsWith("{") ? `<>${children}</>` : children;
22710
+ const preamble = loop.preamble?.ssrText;
22711
+ if (preamble) {
22712
+ return `{${loop.array}.map((${loop.param}${indexParam}) => { ${preamble} return ${safeChildren} })}`;
22713
+ }
22284
22714
  return `{${loop.array}.map((${loop.param}${indexParam}) => ${safeChildren})}`;
22285
22715
  }
22286
22716
  renderComponent(comp) {
@@ -22481,8 +22911,13 @@ function collectLoopBoundNames(ir) {
22481
22911
  for (const child of nested.children)
22482
22912
  visit3(child);
22483
22913
  }
22484
- for (const frag of node.flatMapCallback?.fragments ?? []) {
22485
- visit3(frag.ir);
22914
+ for (const seg of node.flatMapCallback?.segments ?? []) {
22915
+ if (seg.kind === "jsx")
22916
+ visit3(seg.ir);
22917
+ }
22918
+ for (const seg of node.preamble?.segments ?? []) {
22919
+ if (seg.kind === "jsx")
22920
+ visit3(seg.ir);
22486
22921
  }
22487
22922
  break;
22488
22923
  case "conditional":
@@ -23058,12 +23493,16 @@ function restNamesMisused(loop, names) {
23058
23493
  check(l.array);
23059
23494
  check(l.templateArray);
23060
23495
  check(l.key);
23061
- check(l.mapPreamble);
23062
- check(l.templateMapPreamble);
23496
+ if (l.preamble) {
23497
+ check(preambleAnalysisText(l.preamble));
23498
+ check(preambleAnalysisTemplateText(l.preamble));
23499
+ }
23063
23500
  if (l.flatMapCallback) {
23064
- check(l.flatMapCallback.body);
23065
- check(l.flatMapCallback.templateBody);
23066
- l.flatMapCallback.fragments.forEach((f) => visit3(f.ir));
23501
+ check(preambleAnalysisText(l.flatMapCallback));
23502
+ for (const seg of l.flatMapCallback.segments) {
23503
+ if (seg.kind === "jsx")
23504
+ visit3(seg.ir);
23505
+ }
23067
23506
  }
23068
23507
  l.children.forEach(visit3);
23069
23508
  };