@barefootjs/jsx 0.34.0 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/analyzer-context.d.ts.map +1 -1
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/index.d.ts +2 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1072 -903
  6. package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
  7. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
  12. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +11 -0
  14. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
  16. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts +52 -0
  19. package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
  20. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +5 -2
  21. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +24 -44
  23. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +6 -7
  25. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
  27. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
  30. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  31. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -6
  32. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  33. package/dist/ir-to-client-js/control-flow/shared.d.ts +11 -9
  34. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  35. package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
  36. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
  37. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  38. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
  39. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  40. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
  41. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  42. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  43. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
  44. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  45. package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
  46. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  47. package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
  48. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  49. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  50. package/dist/ir-to-client-js/html-template.d.ts +2 -2
  51. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  52. package/dist/ir-to-client-js/imports.d.ts +2 -2
  53. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  54. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  55. package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
  56. package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
  57. package/dist/ir-to-client-js/reactivity.d.ts +62 -18
  58. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  59. package/dist/ir-to-client-js/safe-html.d.ts +158 -0
  60. package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
  61. package/dist/ir-to-client-js/types.d.ts +18 -2
  62. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  63. package/dist/ir-to-client-js/utils.d.ts +34 -1
  64. package/dist/ir-to-client-js/utils.d.ts.map +1 -1
  65. package/dist/jsx-to-ir.d.ts.map +1 -1
  66. package/dist/prop-rewrite.d.ts +8 -3
  67. package/dist/prop-rewrite.d.ts.map +1 -1
  68. package/dist/props-binding.d.ts +22 -1
  69. package/dist/props-binding.d.ts.map +1 -1
  70. package/dist/reactivity-checker.d.ts +18 -0
  71. package/dist/reactivity-checker.d.ts.map +1 -1
  72. package/dist/ssr-defaults.d.ts.map +1 -1
  73. package/dist/types.d.ts +5 -10
  74. package/dist/types.d.ts.map +1 -1
  75. package/package.json +2 -2
  76. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +138 -22
  77. package/src/__tests__/binding-scope-ratchet.test.ts +25 -2
  78. package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
  79. package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
  80. package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
  81. package/src/__tests__/compiler-stress-1244.test.ts +8 -8
  82. package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
  83. package/src/__tests__/flatmap-segments.test.ts +1 -1
  84. package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
  85. package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
  86. package/src/__tests__/lazy-conditional.test.ts +52 -12
  87. package/src/__tests__/lazy-preamble.test.ts +11 -11
  88. package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
  89. package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
  90. package/src/__tests__/loop-index-reactivity.test.ts +208 -0
  91. package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
  92. package/src/__tests__/map-arbitrary-body.test.ts +1 -1
  93. package/src/__tests__/map-body-brand-checker.test.ts +176 -0
  94. package/src/__tests__/nested-loop-index-param.test.ts +41 -8
  95. package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
  96. package/src/__tests__/nested-loop-plain.test.ts +39 -25
  97. package/src/__tests__/nested-loop-reactive-attrs.test.ts +16 -6
  98. package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
  99. package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
  100. package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
  101. package/src/__tests__/safe-html-splice-door.test.ts +61 -0
  102. package/src/__tests__/ssr-defaults.test.ts +59 -0
  103. package/src/__tests__/style-css-var-reactive.test.ts +4 -1
  104. package/src/analyzer-context.ts +12 -10
  105. package/src/analyzer.ts +4 -0
  106. package/src/index.ts +8 -2
  107. package/src/ir-to-client-js/collect-elements.ts +126 -32
  108. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
  109. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
  110. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +12 -6
  111. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +36 -59
  112. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +25 -4
  113. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +36 -9
  114. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
  115. package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +116 -0
  116. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +23 -8
  117. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
  118. package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
  119. package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
  120. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
  121. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
  122. package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
  123. package/src/ir-to-client-js/control-flow/shared.ts +40 -17
  124. package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
  125. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
  126. package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
  127. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +22 -15
  128. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
  129. package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
  130. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
  131. package/src/ir-to-client-js/csr-substitute.ts +29 -0
  132. package/src/ir-to-client-js/emit-reactive.ts +92 -6
  133. package/src/ir-to-client-js/emit-registration.ts +2 -4
  134. package/src/ir-to-client-js/html-template.ts +67 -245
  135. package/src/ir-to-client-js/imports.ts +3 -0
  136. package/src/ir-to-client-js/index.ts +4 -4
  137. package/src/ir-to-client-js/markup-slots.ts +25 -0
  138. package/src/ir-to-client-js/reactivity.ts +73 -27
  139. package/src/ir-to-client-js/safe-html.ts +243 -0
  140. package/src/ir-to-client-js/types.ts +19 -3
  141. package/src/ir-to-client-js/utils.ts +58 -10
  142. package/src/jsx-to-ir.ts +136 -24
  143. package/src/prop-rewrite.ts +8 -4
  144. package/src/props-binding.ts +37 -1
  145. package/src/reactivity-checker.ts +60 -0
  146. package/src/ssr-defaults.ts +45 -0
  147. package/src/types.ts +5 -10
  148. package/src/__tests__/text-slot-escaping.test.ts +0 -65
@@ -7,8 +7,18 @@
7
7
  * - Sibling levels at the same depth are flattened into the top-level
8
8
  * list; deeper levels become `childLevels` of the immediately-preceding
9
9
  * ancestor.
10
- * - Choose `reactive` vs `static` mode by checking whether `inner.array`
11
- * references the *current* outer loop param (the O-8 narrowing fix).
10
+ * - Every level gets the full reactive `mapArray` emission (#2865
11
+ * the former "static forEach, setup-only" shortcut for a nested
12
+ * `.map()` whose array didn't textually mention the outer loop's item
13
+ * silently froze any OTHER reactivity the inner array or its rows
14
+ * depended on: a signal, a memo, a prop, an imported function call, a
15
+ * preamble local, a destructured outer param, an outer-index-derived
16
+ * array — none of those are "the outer item", so `refsParent` missed
17
+ * all of them, and the static emitter never wired a single
18
+ * `createEffect` for the row's own reactiveTexts/reactiveAttrs/
19
+ * conditionals either. A `mapArray` over a genuinely non-reactive
20
+ * array just subscribes to nothing and runs once — a strict superset
21
+ * of what the deleted static path did).
12
22
  * - Recurse with `inner.param` as the new outerLoopParam so deeper
13
23
  * levels see their immediate parent.
14
24
  */
@@ -28,7 +38,7 @@ import {
28
38
  wrapLoopParamAsAccessor,
29
39
  attrValueToString,
30
40
  } from '../../utils.ts'
31
- import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
41
+ import { buildChildRefBindings } from '../shared.ts'
32
42
  import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
33
43
  import { buildLoopChildConditionalsPlan } from './build-loop-child-arm.ts'
34
44
  import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
@@ -67,7 +77,6 @@ import type {
67
77
  InnerLoopPlan,
68
78
  InnerLoopReactiveAttr,
69
79
  InnerLoopReactiveEmit,
70
- InnerLoopStaticEmit,
71
80
  InnerLoopText,
72
81
  InnerLoopsPlan,
73
82
  } from './inner-loop.ts'
@@ -77,6 +86,8 @@ export interface BuildInnerLoopsArgs {
77
86
  parentElVar: string
78
87
  outerLoopParam: string | undefined
79
88
  outerLoopParamBindings?: readonly LoopParamBinding[]
89
+ /** Outer loop's index param name, when present (#2861). */
90
+ outerLoopIndex?: string | null
80
91
  }
81
92
 
82
93
  /**
@@ -85,9 +96,9 @@ export interface BuildInnerLoopsArgs {
85
96
  * conditional branch are skipped (they're emitted by `stringifyBranchInnerLoops`).
86
97
  */
87
98
  export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
88
- const { levels, parentElVar, outerLoopParam, outerLoopParamBindings } = args
99
+ const { levels, parentElVar, outerLoopParam, outerLoopParamBindings, outerLoopIndex } = args
89
100
  const wrapOuter = outerLoopParam
90
- ? (expr: string) => wrapLoopParamAsAccessor(expr, outerLoopParam, outerLoopParamBindings)
101
+ ? (expr: string) => wrapLoopParamAsAccessor(expr, outerLoopParam, outerLoopParamBindings, outerLoopIndex)
91
102
  : (expr: string) => expr
92
103
 
93
104
  const plan: InnerLoopPlan[] = []
@@ -111,17 +122,13 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
111
122
  ? `qsa(${parentElVar}, '[bf="${inner.containerSlotId}"]')`
112
123
  : parentElVar
113
124
 
114
- // O-8 narrowing: at depth 2+, `inner.array` may reference the *immediate*
115
- // parent loop's param. Check against whatever outerLoopParam our caller
116
- // narrowed to the recursion passes `inner.param` for child levels.
117
- const refsParent = !!outerLoopParam && (inner.arrayFreeIdentifiers?.has(outerLoopParam) ?? false)
118
- const useReactive = refsParent && !!inner.template
125
+ // #2865: every nested loop gets the full reactive `mapArray` emission,
126
+ // regardless of whether its array references the outer loop's item —
127
+ // see the module docstring for why the old `refsParent`-gated static
128
+ // shortcut was unsound.
129
+ const emit: InnerLoopReactiveEmit = buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings, outerLoopIndex)
119
130
 
120
- const emit: InnerLoopReactiveEmit | InnerLoopStaticEmit = useReactive
121
- ? buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings)
122
- : buildStaticEmit(inner, level, uidSuffix)
123
-
124
- const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array
131
+ const arrayExpr = wrapOuter(inner.array)
125
132
 
126
133
  const childLevelsPlan = childLevels.length > 0
127
134
  ? buildInnerLoopsPlan({
@@ -129,6 +136,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
129
136
  parentElVar: `__innerEl${uidSuffix}`,
130
137
  outerLoopParam: inner.param,
131
138
  outerLoopParamBindings: inner.paramBindings,
139
+ outerLoopIndex: inner.index,
132
140
  })
133
141
  : []
134
142
 
@@ -146,6 +154,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
146
154
  childLevels: childLevelsPlan,
147
155
  outerLoopParam,
148
156
  outerLoopParamBindings,
157
+ outerLoopIndex,
149
158
  })
150
159
 
151
160
  i = j
@@ -160,12 +169,13 @@ function buildReactiveEmit(
160
169
  uidSuffix: string,
161
170
  outerLoopParam?: string,
162
171
  outerLoopParamBindings?: readonly LoopParamBinding[],
172
+ outerLoopIndex?: string | null,
163
173
  ): InnerLoopReactiveEmit {
164
- const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
165
- const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
174
+ const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
175
+ const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
166
176
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
167
177
  const wrappedKey = inner.key
168
- ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
178
+ ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index)
169
179
  : null
170
180
 
171
181
  // Inner-wrap children IR recursively so nested component props (e.g.,
@@ -202,12 +212,12 @@ function buildReactiveEmit(
202
212
 
203
213
  const reactiveTexts: InnerLoopText[] = inner.bindings.reactiveTexts.map(text => ({
204
214
  slotId: text.slotId,
205
- wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings),
215
+ wrappedExpression: wrapLoopParamAsAccessor(wrapOuter(text.expression), inner.param, inner.paramBindings, inner.index),
206
216
  insideConditional: !!text.insideConditional,
207
217
  }))
208
218
 
209
219
  const reactiveAttrs: InnerLoopReactiveAttr[] = inner.bindings.reactiveAttrs.map(attr => {
210
- const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings)
220
+ const wrapped = wrapLoopParamAsAccessor(wrapOuter(attr.expression), inner.param, inner.paramBindings, inner.index)
211
221
  return {
212
222
  slotId: attr.childSlotId,
213
223
  attrName: attr.attrName,
@@ -240,17 +250,17 @@ function buildReactiveEmit(
240
250
  // wrapInner(wrapOuter(...)) applied to the js text.
241
251
  const leafLoopParams = outerLoopParam
242
252
  ? [
243
- { param: outerLoopParam, bindings: outerLoopParamBindings },
244
- { param: inner.param, bindings: inner.paramBindings },
253
+ { param: outerLoopParam, bindings: outerLoopParamBindings, index: outerLoopIndex },
254
+ { param: inner.param, bindings: inner.paramBindings, index: inner.index },
245
255
  ]
246
- : [{ param: inner.param, bindings: inner.paramBindings }]
256
+ : [{ param: inner.param, bindings: inner.paramBindings, index: inner.index }]
247
257
  preludeStatements.push(renderPreamble(inner.preamble, {
248
258
  transformJs: (t) => wrapInner(wrapOuter(t)),
249
259
  renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined),
250
260
  }))
251
261
  }
252
262
 
253
- const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
263
+ const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings, inner.index)
254
264
 
255
265
  // Per-item conditionals inside THIS loop's own row (#2706) — same
256
266
  // insert()-parity treatment the top-level loop's row conditionals
@@ -265,6 +275,7 @@ function buildReactiveEmit(
265
275
  wrap: wrapBoth,
266
276
  loopParam: inner.param,
267
277
  loopParamBindings: inner.paramBindings,
278
+ loopIndex: inner.index,
268
279
  })
269
280
 
270
281
  return {
@@ -283,37 +294,3 @@ function buildReactiveEmit(
283
294
  childRefs,
284
295
  }
285
296
  }
286
-
287
- function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string): InnerLoopStaticEmit {
288
- // Static `forEach` iterates with the literal item as its first param, so
289
- // no signal-accessor rewrite is needed — emit the preamble verbatim
290
- // before the component/event setup so prop getters and event handlers
291
- // can resolve the locals (#1064). Refs follow the same contract:
292
- // wrapping the callback would rewrite `s.x` to `s().x` and throw at
293
- // runtime when the callback closes over the static inner param (#1244,
294
- // PR #1352 Copilot review).
295
- //
296
- // The index alias (#2218) lands first, same rationale as the reactive
297
- // path: `forEach`'s second param is the synthetic `__innerIdx<uid>`, not
298
- // the user's index name, so a preamble/prop/event that reads it needs the
299
- // alias bound before anything else runs.
300
- const preludeStatements: string[] = []
301
- const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events)
302
- if (indexAlias) preludeStatements.push(indexAlias)
303
- // Static `forEach` receives the literal item, so neither the js text nor a
304
- // leaf's param reads get accessor-wrapped (mirrors the verbatim emission
305
- // of the js segments above).
306
- if (inner.preamble) {
307
- preludeStatements.push(renderPreamble(inner.preamble, {
308
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
309
- }))
310
- }
311
- return {
312
- mode: 'static',
313
- rawKey: inner.key ?? null,
314
- preludeStatements,
315
- components: level.comps,
316
- events: level.events,
317
- childRefs: buildStaticChildRefBindings(inner.bindings.refs),
318
- }
319
- }
@@ -156,6 +156,17 @@ export interface LazyRowPlanData {
156
156
  * both parsed once per loop and clones on a flip.
157
157
  */
158
158
  conditionals: readonly LazyRowConditionalBinding[]
159
+ /**
160
+ * At least one binding, condition, or preamble-substituted dependency reads
161
+ * the loop's index parameter (#2859 follow-up — `ClassifiedLazyBinding.
162
+ * referencesIndex`, which also forces `readsItem: true` for that binding).
163
+ * The stringifier binds `const <indexParam> = __e.index` at the top of
164
+ * `applyItem` and inside `applyOuter`'s per-entry loop only when this is
165
+ * true, and emits `indexDriven: true` on the plan object literal so the
166
+ * runtime (`mapArrayLazy`) also calls `applyItem` on a pure reorder — a
167
+ * position change with no item change — not just an item change.
168
+ */
169
+ readsIndex: boolean
159
170
  }
160
171
 
161
172
  export interface BuildLazyRowArgs {
@@ -198,7 +209,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
198
209
  // bodies BEFORE classifying, because a binding that reads a declared local
199
210
  // inherits the preamble's dependencies rather than its own literal names.
200
211
  const primableNames = new Set<string>([...scope.signals.keys(), ...scope.memos])
201
- const preambleAnalysis = analyzeLazyPreamble(loop.preamble, args.indexParam, primableNames)
212
+ const preambleAnalysis = analyzeLazyPreamble(loop.preamble, primableNames)
202
213
 
203
214
  // §9.5 conditional widening: a row conditional whose arms are wiring-free
204
215
  // static elements is driven from the apply bodies instead of a per-row
@@ -207,9 +218,14 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
207
218
  const condFacts: LazyConditionalFacts[] = []
208
219
  let conditionalRefusal: string | null = null
209
220
  for (const cond of rawConditionals) {
210
- const verdict = analyzeLazyConditional(cond, args.indexParam, {
211
- whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
212
- whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
221
+ // whenTrueHtml/whenFalseHtml are already loop-param-wrapped at IR
222
+ // render time (`irToHtmlTemplate`'s `loopParams`, collect-elements.ts)
223
+ // — do NOT re-wrap the rendered HTML string here (#2868: a
224
+ // word-boundary regex over assembled markup can match a bare tag name
225
+ // colliding with the param/index identifier, e.g. `<i>` -> `<i()>`).
226
+ const verdict = analyzeLazyConditional(cond, {
227
+ whenTrueHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
228
+ whenFalseHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
213
229
  })
214
230
  if (!verdict.lazySafe) { conditionalRefusal = verdict.reason; break }
215
231
  condFacts.push(verdict.facts)
@@ -374,6 +390,11 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
374
390
  itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsItem && b.readsPreamble),
375
391
  outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsOuter && b.readsPreamble),
376
392
  conditionals,
393
+ // From the RAW classification, not the final lists: a binding that
394
+ // reads the index is always `readsItem` (see `classifyLazyBinding`),
395
+ // so checking the final lists would say the same thing — raw is just
396
+ // the more direct source of truth.
397
+ readsIndex: classified.some(c => c.referencesIndex),
377
398
  },
378
399
  decision,
379
400
  }
@@ -202,6 +202,8 @@ export interface BuildBranchInnerLoopsArgs {
202
202
  outerLoopParam: string
203
203
  /** Outer loop param destructuring metadata. */
204
204
  outerLoopParamBindings?: readonly LoopParamBinding[]
205
+ /** Outer loop's index param name, when present (#2861). */
206
+ outerLoopIndex?: string | null
205
207
  /**
206
208
  * Outer-wrap closure — defaults to wrapping with `outerLoopParam`. Overridden
207
209
  * by `emitNestedLoopChildConditionals` recursion (which threads its own wrap).
@@ -225,6 +227,7 @@ export function buildBranchInnerLoopsPlan(
225
227
  condSlotId,
226
228
  outerLoopParam,
227
229
  outerLoopParamBindings,
230
+ outerLoopIndex,
228
231
  wrapOuter,
229
232
  } = args
230
233
  if (!innerLoops || innerLoops.length === 0) return []
@@ -232,10 +235,15 @@ export function buildBranchInnerLoopsPlan(
232
235
  const plan: BranchInnerLoop[] = []
233
236
  for (let i = 0; i < innerLoops.length; i++) {
234
237
  const inner = innerLoops[i]
235
- if (!inner.refsOuterParam || !inner.template) continue
238
+ // #2865: an inner loop inside a conditional arm used to be dropped
239
+ // entirely (no `mapArray`, no hydration wiring at all) whenever its
240
+ // array didn't reference the outer loop's item — the same unsound
241
+ // gate `build-inner-loop.ts` used, just answered by skipping instead
242
+ // of falling back to a weaker static emission. Always build the plan.
243
+ if (!inner.template) continue
236
244
 
237
- const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
238
- const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
245
+ const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
246
+ const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
239
247
 
240
248
  const csl = inner.containerSlotId
241
249
  // Inner loop's container: host-side `bf="<slot>"` slot marker first,
@@ -251,7 +259,7 @@ export function buildBranchInnerLoopsPlan(
251
259
 
252
260
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
253
261
  const wrappedKey = inner.key
254
- ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
262
+ ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings, inner.index)
255
263
  : null
256
264
 
257
265
  // Inner-wrap children IR recursively so nested component props (e.g.
@@ -327,11 +335,13 @@ export function buildBranchInnerLoopsPlan(
327
335
  wrap: wrapBoth,
328
336
  loopParam: inner.param,
329
337
  loopParamBindings: inner.paramBindings,
338
+ loopIndex: inner.index,
330
339
  }),
331
340
  innerLoopParam: inner.param,
332
341
  innerLoopParamBindings: inner.paramBindings,
333
342
  outerLoopParam,
334
343
  outerLoopParamBindings,
344
+ outerLoopIndex,
335
345
  })
336
346
  }
337
347
  return plan
@@ -352,6 +362,8 @@ export interface BuildLoopChildConditionalsArgs {
352
362
  loopParam: string
353
363
  /** Loop param destructuring metadata. */
354
364
  loopParamBindings?: readonly LoopParamBinding[]
365
+ /** Loop's index param name, when it declares one (#2861) — same role as `loopParam` above. */
366
+ loopIndex?: string | null
355
367
  }
356
368
 
357
369
  /**
@@ -360,7 +372,8 @@ export interface BuildLoopChildConditionalsArgs {
360
372
  * recursion. Each conditional pre-builds:
361
373
  *
362
374
  * - the wrapped condition expression
363
- * - the wrapped + addCondAttr'd whenTrue / whenFalse template HTML
375
+ * - the addCondAttr'd whenTrue / whenFalse template HTML (already
376
+ * loop-param-wrapped at IR render time, #2868)
364
377
  * - per-arm `LoopChildArmPlan`: events / child components / inner
365
378
  * loops / nested conditionals (recursion)
366
379
  *
@@ -370,7 +383,7 @@ export interface BuildLoopChildConditionalsArgs {
370
383
  export function buildLoopChildConditionalsPlan(
371
384
  args: BuildLoopChildConditionalsArgs,
372
385
  ): LoopChildConditionalPlan[] {
373
- const { conditionals, scopeVar, wrap, loopParam, loopParamBindings } = args
386
+ const { conditionals, scopeVar, wrap, loopParam, loopParamBindings, loopIndex } = args
374
387
  if (!conditionals || conditionals.length === 0) return []
375
388
 
376
389
  const plans: LoopChildConditionalPlan[] = []
@@ -379,13 +392,20 @@ export function buildLoopChildConditionalsPlan(
379
392
  slotId: cond.slotId,
380
393
  scopeVar,
381
394
  wrappedCondition: wrap(cond.condition),
382
- whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
383
- whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
395
+ // whenTrueHtml/whenFalseHtml are already loop-param-wrapped for the
396
+ // full ancestor chain at IR render time (`irToHtmlTemplate`'s
397
+ // `loopParams`, collect-elements.ts) — do NOT re-wrap the rendered
398
+ // HTML string here (#2868: a word-boundary regex over assembled
399
+ // markup can match a bare tag name colliding with the param/index
400
+ // identifier, e.g. `<i>` -> `<i()>`).
401
+ whenTrueTemplateHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
402
+ whenFalseTemplateHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
384
403
  whenTrueArm: buildLoopChildArmPlan({
385
404
  branch: cond.whenTrue,
386
405
  wrap,
387
406
  loopParam,
388
407
  loopParamBindings,
408
+ loopIndex,
389
409
  condId: cond.slotId,
390
410
  }),
391
411
  whenFalseArm: buildLoopChildArmPlan({
@@ -393,8 +413,10 @@ export function buildLoopChildConditionalsPlan(
393
413
  wrap,
394
414
  loopParam,
395
415
  loopParamBindings,
416
+ loopIndex,
396
417
  condId: cond.slotId,
397
418
  }),
419
+ ...(cond.readsPreamble && { readsPreamble: true }),
398
420
  })
399
421
  }
400
422
  return plans
@@ -429,6 +451,7 @@ export function buildArmAttrsPlan(
429
451
  attrName: attr.attrName,
430
452
  wrappedExpression: wrap(attr.expression),
431
453
  meta: pickAttrMeta(attr),
454
+ ...(attr.readsPreamble && { readsPreamble: true }),
432
455
  })),
433
456
  })
434
457
  }
@@ -455,12 +478,14 @@ interface BuildLoopChildArmArgs {
455
478
  wrap: (expr: string) => string
456
479
  loopParam: string
457
480
  loopParamBindings?: readonly LoopParamBinding[]
481
+ /** Loop's index param name, when it declares one (#2861). */
482
+ loopIndex?: string | null
458
483
  /** The enclosing conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
459
484
  condId: string
460
485
  }
461
486
 
462
487
  function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
463
- const { branch, wrap, loopParam, loopParamBindings, condId } = args
488
+ const { branch, wrap, loopParam, loopParamBindings, loopIndex, condId } = args
464
489
  return {
465
490
  events: buildBranchEventBindingsPlan({
466
491
  events: branch.events,
@@ -476,6 +501,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
476
501
  condSlotId: condId,
477
502
  outerLoopParam: loopParam,
478
503
  outerLoopParamBindings: loopParamBindings,
504
+ outerLoopIndex: loopIndex,
479
505
  wrapOuter: wrap,
480
506
  }),
481
507
  nestedConditionals: buildLoopChildConditionalsPlan({
@@ -484,6 +510,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
484
510
  wrap,
485
511
  loopParam,
486
512
  loopParamBindings,
513
+ loopIndex,
487
514
  }),
488
515
  attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
489
516
  texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
@@ -28,17 +28,14 @@ import {
28
28
  buildLoopChildIndexExpr,
29
29
  setIntersects,
30
30
  varSlotId,
31
- wrapLoopParamAsAccessor,
32
31
  } from '../../utils.ts'
33
32
  import {
34
33
  loopKeyFn,
35
- destructureLoopParam,
36
34
  buildChildRefBindings,
37
35
  buildStaticChildRefBindings,
38
- buildPreambleRegionPlans,
39
36
  } from '../shared.ts'
40
37
  import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
41
- import { buildLazyRowPlan } from './build-lazy-row.ts'
38
+ import { buildPlainRowCore } from './build-plain-row.ts'
42
39
  import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
43
40
  import { buildComponentLoopPlan } from './build-component-loop.ts'
44
41
  import { buildTopLevelCompositePlan } from './build-composite-loop.ts'
@@ -99,8 +96,6 @@ export function buildPlainLoopPlan(
99
96
  profileComponentName?: string,
100
97
  lazyScope?: LazyRowScopeInfo,
101
98
  ): PlainLoopPlan {
102
- const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings)
103
- const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
104
99
  const hasReactive = elem.bindings.reactiveAttrs.length > 0
105
100
  || elem.bindings.reactiveTexts.length > 0
106
101
  || elem.bindings.conditionals.length > 0
@@ -140,14 +135,26 @@ export function buildPlainLoopPlan(
140
135
  }
141
136
 
142
137
  const arrayExpr = buildChainedArrayExpr(elem)
143
- const indexParam = elem.index || '__idx'
144
- const mapPreambleWrapped = elem.preamble
145
- ? renderPreamble(elem.preamble, {
146
- transformJs: wrap,
147
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
148
- })
149
- : ''
150
- const preambleRegions = buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings)
138
+
139
+ // Shared "wrap item → decide lazy → wrap index" core (#2859 follow-up) —
140
+ // see `build-plain-row.ts`'s module docstring for why this single
141
+ // implementation matters: a lazy-eligible loop's `LazyRowPlanData` embeds
142
+ // `mapPreambleWrapped` and clones from `elem.template` verbatim, and
143
+ // `mapArrayLazy`'s `createRow(entry, index)` hands the row a plain index
144
+ // NUMBER, never an accessor — wrapping unconditionally would call a number
145
+ // there. Lazy-row eligibility only refuses a REACTIVE binding that
146
+ // references the index; a non-reactive use (e.g. a `data-key` built from
147
+ // the index) is not gated the same way, so "lazy-eligible" does not imply
148
+ // "no index in the template" either.
149
+ const core = buildPlainRowCore({
150
+ loop: elem,
151
+ arrayExpr,
152
+ callSite: 'plain',
153
+ flatMapLeafItem: false,
154
+ anchored: elem.bodyIsItemConditional ?? false,
155
+ scope: lazyScope,
156
+ })
157
+ const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow, wrapItem: wrap } = core
151
158
 
152
159
  return {
153
160
  kind: 'plain',
@@ -160,29 +167,21 @@ export function buildPlainLoopPlan(
160
167
  paramHead,
161
168
  paramUnwrap,
162
169
  indexParam,
163
- // Lazy row graph (§9, L3). `null` for every ineligible loop, which then
164
- // keeps the eager emission below byte-for-byte.
165
- lazyRow: buildLazyRowPlan({
166
- loop: elem,
167
- arrayExpr,
168
- indexParam,
169
- paramUnwrap,
170
- mapPreambleWrapped,
171
- preambleRegionCount: preambleRegions.length,
172
- callSite: 'plain',
173
- flatMapLeafItem: false,
174
- anchored: elem.bodyIsItemConditional ?? false,
175
- scope: lazyScope,
176
- }) ?? undefined,
170
+ lazyRow,
177
171
  // Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
178
172
  // render as HTML-string templates under this loop's param context so a
179
173
  // leaf that reads the item (`r`) becomes `r()`.
180
- mapPreambleWrapped,
181
- template: elem.template,
174
+ mapPreambleWrapped: core.mapPreambleWrapped,
175
+ template: core.template,
182
176
  skeletonTemplate: elem.skeletonTemplate,
183
177
  skeletonPaths: elem.skeletonPaths,
184
178
  reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
185
- childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
179
+ // `elem.index` (#2859 follow-up): a ref callback closing over the loop
180
+ // index (`ref={el => refs[i] = el}`) must see it wrapped the same way
181
+ // every other reference in this row is — a row with any ref is always
182
+ // lazy-ineligible (`lazy-row-eligibility.ts`'s `childRefCount` gate), so
183
+ // there is no shared-string hazard here, unlike `mapPreambleWrapped`.
184
+ childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
186
185
  preambleRegions,
187
186
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
188
187
  anchored: elem.bodyIsItemConditional ?? false,
@@ -190,9 +189,10 @@ export function buildPlainLoopPlan(
190
189
  // conditional without a key is a BF023 error, but the emitted client JS
191
190
  // must still parse — an empty `anchorKeyExpr` would produce
192
191
  // `createComment(`bf-loop-i:${}`)` (a SyntaxError that breaks the whole
193
- // bundle). `elem.index || '__idx'` matches `indexParam` above, so the
194
- // anchor value stays consistent with the renderItem's own index param.
195
- anchorKeyExpr: elem.key ? wrap(elem.key) : (elem.index || '__idx'),
192
+ // bundle). `indexParam` matches the renderItem head built above, and
193
+ // (#2859) is bound to an INDEX ACCESSOR at runtime, not a plain number —
194
+ // call it, same as every other reference to it in this row's body.
195
+ anchorKeyExpr: elem.key ? wrap(elem.key) : `${indexParam}()`,
196
196
  }
197
197
  }
198
198
 
@@ -269,7 +269,7 @@ function buildStaticLoopMaterialize(
269
269
  itemTemplate: elem.staticItemTemplate,
270
270
  mapPreamble: elem.preamble
271
271
  ? renderPreamble(elem.preamble, {
272
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
272
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings, index: elem.index }], undefined),
273
273
  })
274
274
  : '',
275
275
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared "wrap item → decide lazy → wrap index" core for a plain loop row.
3
+ *
4
+ * `buildPlainLoopPlan` (`build-loop.ts`, `TopLevelLoop`) and
5
+ * `buildBranchLoopPlan` (`build-branch-loop.ts`, `BranchLoop`) both build a
6
+ * row from a `.map()` callback the SAME way:
7
+ *
8
+ * 1. Wrap the row's JS with ITEM-ONLY accessor rewriting (no index) — this
9
+ * string must stay index-unwrapped because it is reused VERBATIM by the
10
+ * lazy row plan below, which reads the index differently (a live
11
+ * per-call value off `entry.index`/`createRow`'s parameter, never a
12
+ * `.map()`-body call).
13
+ * 2. Ask `buildLazyRowPlan` for a verdict.
14
+ * 3. Only once lazy is ruled out (`!lazyRow`), wrap index references too,
15
+ * for the eager path — `mapArrayLazy`'s `createRow` hands a row a plain
16
+ * index NUMBER, so wrapping unconditionally would call a number.
17
+ *
18
+ * Before this extraction, the sequence was duplicated verbatim (mod `elem`/`loop` naming)
19
+ * between the two builders — exactly the "one decision, two implementations"
20
+ * pattern this repo's CLAUDE.md warns against, and the shape of the original
21
+ * #2859 CI regression (the eager index-wrap leaking into the lazy-shared
22
+ * string). One implementation now, called from both sites, so the decision
23
+ * cannot silently diverge again.
24
+ */
25
+
26
+ import type { BranchLoop, TopLevelLoop } from '../../types.ts'
27
+ import { wrapLoopParamAsAccessor, wrapIndexParamAsAccessor } from '../../utils.ts'
28
+ import { destructureLoopParam, buildPreambleRegionPlans } from '../shared.ts'
29
+ import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
30
+ import { buildLazyRowPlan, type LazyRowPlanData } from './build-lazy-row.ts'
31
+ import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
32
+ import type { PreambleRegionPlan } from './loop.ts'
33
+
34
+ export interface PlainRowInputs {
35
+ loop: TopLevelLoop | BranchLoop
36
+ /** Fully-chained array expression as the CALLER emits it (branch-loop's flatMap form differs from plain's). */
37
+ arrayExpr: string
38
+ callSite: 'plain' | 'branch-plain'
39
+ flatMapLeafItem: boolean
40
+ anchored: boolean
41
+ scope: LazyRowScopeInfo | undefined
42
+ }
43
+
44
+ export interface PlainRowCore {
45
+ indexParam: string
46
+ paramHead: string
47
+ paramUnwrap: string
48
+ preambleRegions: readonly PreambleRegionPlan[]
49
+ lazyRow: LazyRowPlanData | undefined
50
+ /** Item-wrapped, and index-wrapped too IFF the row is NOT lazy-eligible. */
51
+ mapPreambleWrapped: string
52
+ /** Same rule as `mapPreambleWrapped`, applied to `loop.template`. */
53
+ template: string
54
+ /** Item-only wrap — exposed for the few expressions (e.g. `anchorKeyExpr`) built outside this core. */
55
+ wrapItem: (expr: string) => string
56
+ }
57
+
58
+ export function buildPlainRowCore(inputs: PlainRowInputs): PlainRowCore {
59
+ const { loop, arrayExpr, callSite, flatMapLeafItem, anchored, scope } = inputs
60
+
61
+ const wrapItem = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings)
62
+ const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
63
+ const indexParam = loop.index || '__idx'
64
+
65
+ const mapPreambleWrapped = loop.preamble
66
+ ? renderPreamble(loop.preamble, {
67
+ transformJs: wrapItem,
68
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
69
+ })
70
+ : ''
71
+ const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings, loop.index)
72
+
73
+ // Lazy row graph (§9, L3). `undefined` for every ineligible loop, which
74
+ // then keeps the eager emission below byte-for-byte. Decided from the
75
+ // item-only-wrapped `mapPreambleWrapped` above — eligibility itself reads
76
+ // `loop.preamble` (raw), not this string, so which wrap it carries doesn't
77
+ // change the verdict.
78
+ const lazyRow = buildLazyRowPlan({
79
+ loop,
80
+ arrayExpr,
81
+ indexParam,
82
+ paramUnwrap,
83
+ mapPreambleWrapped,
84
+ preambleRegionCount: preambleRegions.length,
85
+ callSite,
86
+ flatMapLeafItem,
87
+ anchored,
88
+ scope,
89
+ }) ?? undefined
90
+
91
+ // Only once lazy is ruled out is it safe to ALSO wrap index references
92
+ // (#2859 follow-up) — see the module docstring.
93
+ const mapPreambleWrappedFinal = !lazyRow && loop.index
94
+ ? wrapIndexParamAsAccessor(mapPreambleWrapped, loop.index)
95
+ : mapPreambleWrapped
96
+ // `loop.templateIndexed` is a second structured render (index wrapped at
97
+ // IR time, `collect-elements.ts`) rather than a post-hoc regex pass over
98
+ // `loop.template` — a word-boundary regex over assembled HTML can match a
99
+ // bare tag name colliding with the index identifier (`<i>` -> `<i()>`,
100
+ // #2868). Falls back to `loop.template` for the rare shape that doesn't
101
+ // populate it (e.g. a flatMap projection loop).
102
+ const templateFinal = !lazyRow && loop.index
103
+ ? (loop.templateIndexed ?? loop.template)
104
+ : loop.template
105
+
106
+ return {
107
+ indexParam,
108
+ paramHead,
109
+ paramUnwrap,
110
+ preambleRegions,
111
+ lazyRow,
112
+ mapPreambleWrapped: mapPreambleWrappedFinal,
113
+ template: templateFinal,
114
+ wrapItem,
115
+ }
116
+ }