@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
@@ -113,7 +113,10 @@ describe('reactive CSS custom properties (#135)', () => {
113
113
 
114
114
  // Both reactive sinks live in the init body — one assigning the DOM
115
115
  // `disabled` property, the other writing the `aria-busy` attribute.
116
- expect(content).toMatch(/\.disabled\s*=\s*!!\(busy\(\)\)/)
116
+ // The write reads a dedup-guarded temp (`__x`, #2869), not `busy()`
117
+ // inline — the computed value is still `busy()`'s result either way.
118
+ expect(content).toMatch(/const __x = busy\(\)/)
119
+ expect(content).toMatch(/\.disabled\s*=\s*!!\(__x\)/)
117
120
  expect(content).toContain("setAttribute('aria-busy'")
118
121
  })
119
122
  })
@@ -26,6 +26,7 @@ import type {
26
26
  } from './types.ts'
27
27
  import { type ExcludeRange, collectAllTypeRanges, reconstructWithoutTypes } from './strip-types.ts'
28
28
  import type { CallbackBodyAcceptor } from './adapters/interface.ts'
29
+ import { nodeContainsJsx } from './reactivity-checker.ts'
29
30
 
30
31
  /**
31
32
  * Deferred info for BF043 (props destructuring warning).
@@ -318,10 +319,17 @@ export function createAnalyzerContext(
318
319
  // travel as structured segments, never as raw text. Every getJS call
319
320
  // site shares this contract; the env gate keeps the subtree walk off
320
321
  // the production hot path (the trichotomy harness enables it). Scoped
321
- // to error-free compiles: a compile that already refused loudly may
322
- // take degraded fallback paths whose artifacts are gated by the error —
323
- // the invariant this trips on is the SILENT leak.
324
- if (process.env.BF_ASSERT_NO_JSX_IN_GETJS === '1' && this.errors.length === 0 && nodeContainsJsx(node)) {
322
+ // to compiles with no `error`-severity diagnostic: a compile that
323
+ // already refused loudly may take degraded fallback paths whose
324
+ // artifacts are gated by the error — the invariant this trips on is
325
+ // the SILENT leak. A `warning` (e.g. BF043 props-destructuring) does
326
+ // not gate anything, so it must not silently disarm this assertion too
327
+ // (#2867) — only `error` severity does.
328
+ if (
329
+ process.env.BF_ASSERT_NO_JSX_IN_GETJS === '1' &&
330
+ !this.errors.some((e) => e.severity === 'error') &&
331
+ nodeContainsJsx(node)
332
+ ) {
325
333
  throw new Error(
326
334
  'getJS() called on a JSX-bearing node — raw JSX must never be spliced ' +
327
335
  'into emitted output. Carry mixed content as structured segments ' +
@@ -336,12 +344,6 @@ export function createAnalyzerContext(
336
344
  }
337
345
  }
338
346
 
339
- /** Subtree JSX check for the test-gated getJS assertion above. */
340
- function nodeContainsJsx(node: ts.Node): boolean {
341
- if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) return true
342
- return ts.forEachChild(node, nodeContainsJsx) ?? false
343
- }
344
-
345
347
  // =============================================================================
346
348
  // Source Location Helper
347
349
  // =============================================================================
package/src/analyzer.ts CHANGED
@@ -1998,6 +1998,9 @@ const CLIENT_EXPORTS = new Set([
1998
1998
  'forwardProps', 'unwrap', '__slot',
1999
1999
  'createContext', 'useContext', 'provideContext',
2000
2000
  'createPortal', 'isSSRPortal', 'findSiblingSlot', 'cleanupPortalPlaceholder',
2001
+ // Floating-element position tracking (#2848) — same runtime-only shape
2002
+ // as the portal entries above.
2003
+ 'trackPosition',
2001
2004
  // Request-scoped environment signal factory (router v0.5) — `createSignal`-
2002
2005
  // shaped, recognised structurally (#2057) so its getter is just a signal
2003
2006
  // getter; the compiler lowers the reader value per adapter via the signal's
@@ -4221,6 +4224,7 @@ export const BROWSER_ONLY_CLIENT_APIS = new Set([
4221
4224
  'isSSRPortal',
4222
4225
  'findSiblingSlot',
4223
4226
  'cleanupPortalPlaceholder',
4227
+ 'trackPosition',
4224
4228
  ])
4225
4229
 
4226
4230
  function importsBrowserOnlyClientApi(ctx: AnalyzerContext): boolean {
package/src/index.ts CHANGED
@@ -11,10 +11,16 @@ export type { CompileResult, CompileOptions, CompileOptionsWithAdapter, FileOutp
11
11
  // SSR template-variable defaults (manifest seeds for stash-based adapters)
12
12
  export { extractSsrDefaults, deriveStashFromDefaults } from './ssr-defaults.ts'
13
13
 
14
- // Shared props-destructure binding + alias-map helpers (#2524)
15
- export { propsDestructureBinding, buildPropAliasMap, isIdentifierName } from './props-binding.ts'
14
+ // Shared props-destructure binding + alias-map helpers (#2524, #2788)
15
+ export { propsDestructureBinding, buildPropAliasMap, isIdentifierName, resolveBodyDestructuredPropAliases } from './props-binding.ts'
16
16
  export type { SsrDefault } from './ssr-defaults.ts'
17
17
 
18
+ // Bare local-const alias-hop resolution for a signal/memo getter name
19
+ // (`const items__alias = items`) — #2778's CSR-template fix, reused (not
20
+ // re-walked) by SSR-side alias resolution (#2813: DSL stash seeding,
21
+ // Go's zero-arg-call field routing).
22
+ export { resolveGetterAliases, collectAliasableGetterNames } from './ir-to-client-js/csr-substitute.ts'
23
+
18
24
  // Import-alias resolution (#2777 client-JS registry key; #2822 SSR cross-template
19
25
  // call names) — a local alias -> declared/exported name map built from
20
26
  // `ir.metadata.imports`. DSL adapters build this once per compile and look up
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp, pickAttrMetaFromIR } from '../types.ts'
6
6
  import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
7
- import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
7
+ import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM, type LoopParamSpec } from './utils.ts'
8
8
  import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope, anyNameIn } from './reactivity.ts'
9
9
  import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, jsxChildrenPropGetterExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
10
10
  import { detectRootNamespaceWrapTag } from './control-flow/stringify/template-parse.ts'
@@ -12,8 +12,8 @@ import { expandDynamicPropValue, expandConstantForReactivity, resolveRestSpreadO
12
12
  import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
13
13
  import { walkIR, stopAt } from './walker.ts'
14
14
  import { buildLoopChainExpr } from '../loop-chain.ts'
15
- import { identifierPattern } from '../identifier-pattern.ts'
16
15
  import { classifyDOMProp } from '@barefootjs/shared'
16
+ import type { BindingScope } from '../scope/binding-scope.ts'
17
17
 
18
18
  /** Expressions that render nothing (0 DOM nodes) — `&&` / `?:` empty branches. */
19
19
  const EMPTY_RENDER_EXPRS = new Set(['null', 'undefined', 'false', "''", '""', '``'])
@@ -272,7 +272,7 @@ export const branchInnerLoopOptions: CollectInnerLoopsOptions = {
272
272
  export function collectInnerLoops(
273
273
  nodes: IRNode[],
274
274
  siblingOffsets: Map<IRLoop, IRNode[]>,
275
- outerLoopParam?: string,
275
+ outerLoopParam?: string | LoopParamSpec,
276
276
  ctx?: ClientJsContext,
277
277
  options?: CollectInnerLoopsOptions,
278
278
  ): NestedLoop[] {
@@ -280,12 +280,29 @@ export function collectInnerLoops(
280
280
  parentSlotId: string | null
281
281
  depth: number
282
282
  insideCond: boolean
283
+ /**
284
+ * Every ancestor loop's own item/index binding, innermost-first
285
+ * (#2861) — `undefined` above the outermost loop. Threaded (not just
286
+ * captured once from `outerLoopParam`) so a THIRD-level-deep loop's row
287
+ * sees BOTH its grandparent's and its immediate parent's bindings, not
288
+ * only the outermost one: each `loop:` visit below pushes its own
289
+ * frame via `buildLoopRowScope(..., scope.bindingScope)` before
290
+ * descending into that loop's own children.
291
+ */
292
+ bindingScope: BindingScope | undefined
283
293
  }
284
294
  const result: NestedLoop[] = []
285
295
  const flat = options?.flatBranchMode === true
286
296
  const fixedDepth = options?.templateDepth
287
297
  const collectBindings = options?.collectItemBindings === true
288
- const initialScope: Scope = { parentSlotId: null, depth: 0, insideCond: false }
298
+ // Normalize to a spec regardless of which shape the caller passed (#2868
299
+ // callers always pass a full spec; a bare string carries no bindings/index
300
+ // of its own) — one shape feeds both the reactivity scope below and
301
+ // `loopParamsForTemplate` inside the `loop:` visitor.
302
+ const outerSpec: LoopParamSpec | undefined =
303
+ typeof outerLoopParam === 'string' ? { param: outerLoopParam } : outerLoopParam
304
+ const outerScope = buildLoopRowScope(outerSpec?.param, outerSpec?.bindings, undefined, outerSpec?.index)
305
+ const initialScope: Scope = { parentSlotId: null, depth: 0, insideCond: false, bindingScope: outerScope }
289
306
 
290
307
  for (const root of nodes) {
291
308
  walkIR<Scope>(root, initialScope, {
@@ -308,14 +325,10 @@ export function collectInnerLoops(
308
325
  // Pass loopParams so expressions are wrapped at generation time (not post-hoc regex).
309
326
  // Forward destructured bindings (#951) so inner-loop template
310
327
  // references to the destructured locals are rewritten.
311
- const loopParamsForTemplate = outerLoopParam
312
- ? [outerLoopParam, { param: n.param, bindings: n.paramBindings }]
328
+ const loopParamsForTemplate = outerSpec
329
+ ? [outerSpec, { param: n.param, bindings: n.paramBindings, index: n.index }]
313
330
  : undefined
314
331
  const template = n.children.map(c => irToPlaceholderTemplate(c, undefined, emitDepth, loopParamsForTemplate)).join('')
315
- // Check if array expression references the outer loop param
316
- const refsOuter = outerLoopParam
317
- ? identifierPattern(outerLoopParam).test(n.array)
318
- : false
319
332
  // Per-item bindings for inner loop body, collected uniformly when
320
333
  // ctx is available: reactiveTexts / reactiveAttrs / refs are each
321
334
  // classified against the loop's OWN param via `classifyReactivity`,
@@ -349,8 +362,8 @@ export function collectInnerLoops(
349
362
  // guarantees the branch's marker is mounted the moment this
350
363
  // effect first runs (issue-2706-nested-loop-conditional-slot
351
364
  // .test.ts's original repro).
352
- bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
353
- bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
365
+ bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index, scope.bindingScope))
366
+ bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index, scope.bindingScope))
354
367
  bindings.refs.push(...collectLoopChildRefs(child))
355
368
  }
356
369
  bindings.conditionals.push(...collectLoopChildConditionals(
@@ -361,6 +374,12 @@ export function collectInnerLoops(
361
374
  n.paramBindings,
362
375
  innerPreambleNames,
363
376
  n.index,
377
+ // Render a branch's HTML against the FULL ancestor chain (outer
378
+ // loop(s) + this loop's own param), not just this loop's own
379
+ // param — an inner-loop conditional arm can read an outer
380
+ // loop's item/index too (#2868).
381
+ loopParamsForTemplate,
382
+ scope.bindingScope,
364
383
  ))
365
384
  }
366
385
 
@@ -412,7 +431,6 @@ export function collectInnerLoops(
412
431
  containerSlotId: scope.parentSlotId,
413
432
  template,
414
433
  preamble: n.preamble,
415
- refsOuterParam: refsOuter,
416
434
  childComponents,
417
435
  insideConditional: !flat && scope.insideCond ? true : undefined,
418
436
  offset: flat ? undefined : resolveLoopOffset(siblingOffsets.get(n)),
@@ -420,7 +438,14 @@ export function collectInnerLoops(
420
438
  })
421
439
  // Branch-mode callers handle deeper nesting via their own collection paths.
422
440
  if (!flat) {
423
- descend({ ...scope, depth: scope.depth + 1 })
441
+ // #2861: push THIS loop's own item/index frame onto the scope
442
+ // stack before descending, so a loop found deeper still (depth 2+)
443
+ // sees every ancestor's bindings, not just the outermost one.
444
+ descend({
445
+ ...scope,
446
+ depth: scope.depth + 1,
447
+ bindingScope: buildLoopRowScope(n.param, n.paramBindings, innerPreambleNames, n.index, scope.bindingScope),
448
+ })
424
449
  }
425
450
  },
426
451
  // fragment / provider / async auto-descend with the same scope.
@@ -455,7 +480,12 @@ function decideLoopRendering(
455
480
  // loop. These only surface for static arrays (gated at the call site via
456
481
  // `isStaticArray && innerLoops.length`) so dynamic child-component loops —
457
482
  // which render through `createComponent` — are unaffected.
458
- const innerLoops = collectInnerLoops(loop.children, siblingOffsets, loop.param, ctx)
483
+ const innerLoops = collectInnerLoops(
484
+ loop.children,
485
+ siblingOffsets,
486
+ { param: loop.param, bindings: loop.paramBindings, index: loop.index },
487
+ ctx,
488
+ )
459
489
  const hasInnerLoops = (innerLoops?.length ?? 0) > 0
460
490
  const useElementReconciliation =
461
491
  !loop.childComponent && !loop.isStaticArray && (hasNestedComps || hasInnerLoops)
@@ -727,6 +757,7 @@ export function collectElements(
727
757
  : decideLoopRendering(l, siblingOffsets, ctx)
728
758
 
729
759
  let template = ''
760
+ let templateIndexed: string | undefined
730
761
  let staticItemTemplate: string | undefined
731
762
  let skeletonTemplate: string | undefined
732
763
  let skeletonPaths: SkeletonSlotPaths | undefined
@@ -755,10 +786,34 @@ export function collectElements(
755
786
  // avoiding post-hoc regex wrapping that corrupts literal attribute values.
756
787
  // Forward destructured bindings (#951) so references like `cfg.color`
757
788
  // in the emitted template literal are rewritten to `__bfItem()[1].color`.
789
+ // NOT `index: l.index` here (#2859 follow-up): this exact template
790
+ // string is reused verbatim by the LAZY row plan when this loop is
791
+ // lazy-eligible (`LazyRowPlanData`'s clone source, and — via
792
+ // `mapPreambleWrapped` — its `preambleStatements`) — `mapArrayLazy`'s
793
+ // `createRow(entry, index)` still hands the row a plain NUMBER
794
+ // (map-array-lazy.ts is untouched by #2859), so baking `i()` in here
795
+ // would call a number. Lazy-row eligibility only refuses a REACTIVE
796
+ // binding that references the index — a non-reactive use like a
797
+ // `data-key` built from the index is not gated the same way, so this
798
+ // can't assume "lazy-eligible ⇒ no index in the template" either.
799
+ // `buildPlainLoopPlan`/`buildBranchLoopPlan` apply the index wrap
800
+ // themselves, AFTER lazy eligibility is decided, only when it's `null`.
758
801
  const loopParamSpec = [{ param: l.param, bindings: l.paramBindings }]
759
802
  template = useElementReconciliation
760
803
  ? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
761
804
  : irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
805
+ // Second structured render, WITH the index wrapped too — used by
806
+ // `build-plain-row.ts` once lazy eligibility rules out the lazy row
807
+ // plan (which needs `template` above, index-unwrapped, verbatim).
808
+ // Rendered here (IR level) rather than via a post-hoc regex pass
809
+ // over `template`, which can corrupt a tag name colliding with the
810
+ // index identifier (`<i>` -> `<i()>`, #2868).
811
+ if (l.index) {
812
+ const loopParamSpecIndexed = [{ param: l.param, bindings: l.paramBindings, index: l.index }]
813
+ templateIndexed = useElementReconciliation
814
+ ? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpecIndexed)
815
+ : irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpecIndexed)
816
+ }
762
817
  // Static-array loops emit a `forEach((param, idx) => ...)` whose body
763
818
  // references the destructured param directly — `__bfItem()` is not in
764
819
  // scope there. Build a second template that skips the loop-param
@@ -815,6 +870,7 @@ export function collectElements(
815
870
  iterationShape: l.iterationShape,
816
871
  objectIteration: l.objectIteration,
817
872
  template,
873
+ templateIndexed,
818
874
  staticItemTemplate,
819
875
  skeletonTemplate,
820
876
  skeletonPaths,
@@ -1165,6 +1221,11 @@ function collectBranchLoops(
1165
1221
  // keeping the template consistent with reactive effect expressions that
1166
1222
  // use `param()` to read the current item value.
1167
1223
  let childTemplate: string
1224
+ let childTemplateIndexed: string | undefined
1225
+ // NOT `index: n.index` here — see the identical note on `loopParamSpec`
1226
+ // above; `BranchLoop.template` is reused verbatim by the lazy row plan
1227
+ // too (`callSite: 'branch-plain'`), and `buildBranchLoopPlan` applies
1228
+ // the index wrap itself once lazy eligibility is known (#2859 follow-up).
1168
1229
  const branchLoopParamSpec = [{ param: n.param, bindings: n.paramBindings }]
1169
1230
  if (projectionInner) {
1170
1231
  childTemplate = '' // descriptor renderItem builds from d.h, not a row template
@@ -1173,6 +1234,15 @@ function collectBranchLoops(
1173
1234
  } else {
1174
1235
  childTemplate = n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpec)).join('')
1175
1236
  }
1237
+ // Second structured render, WITH the index wrapped too — see the
1238
+ // identical `templateIndexed` note on the top-level loop path above
1239
+ // (#2868).
1240
+ if (n.index && !projectionInner) {
1241
+ const branchLoopParamSpecIndexed = [{ param: n.param, bindings: n.paramBindings, index: n.index }]
1242
+ childTemplateIndexed = useElementReconciliation && n.children[0]
1243
+ ? irToPlaceholderTemplate(n.children[0], restNames, 0, branchLoopParamSpecIndexed)
1244
+ : n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpecIndexed)).join('')
1245
+ }
1176
1246
 
1177
1247
  // Collect per-item bindings (events, reactive attrs/texts, refs,
1178
1248
  // conditionals) for ALL branch loops — simple loops also need
@@ -1198,6 +1268,7 @@ function collectBranchLoops(
1198
1268
  iterationShape: n.iterationShape,
1199
1269
  objectIteration: n.objectIteration,
1200
1270
  template: childTemplate,
1271
+ templateIndexed: childTemplateIndexed,
1201
1272
  containerSlotId: containerSlot,
1202
1273
  preamble: n.preamble,
1203
1274
  preambleRegions: n.preambleRegions,
@@ -1425,24 +1496,41 @@ export function collectLoopChildConditionals(
1425
1496
  * `collectLoopChildBindings`'s doc comment (Copilot review on #2595).
1426
1497
  */
1427
1498
  loopIndex?: string | null,
1499
+ /**
1500
+ * The full ancestor loop-param chain to render branch HTML against
1501
+ * (outermost first), when known — e.g. `[outerSpec, innerSpec]` for a
1502
+ * conditional inside an inner `.map()`. Defaults to just
1503
+ * `[{ param: loopParam, bindings: loopParamBindings, index: loopIndex }]`
1504
+ * (this function's own single-loop scope) when omitted, which is correct
1505
+ * for every caller except `collectInnerLoops`'s inner-loop handler, which
1506
+ * passes the full chain so an inner loop's conditional arm can also
1507
+ * reference an OUTER loop's item/index (#2868). Rendering the branch's
1508
+ * HTML against the full chain at IR time — rather than re-wrapping the
1509
+ * assembled HTML string with a word-boundary regex afterward — is also
1510
+ * what makes it safe for a branch root tag name to collide with a
1511
+ * param/index identifier (`<i>`, `<b>`, …): `irToHtmlTemplate` only wraps
1512
+ * JS expression positions, never tag names.
1513
+ */
1514
+ loopParams?: ReadonlyArray<string | LoopParamSpec>,
1515
+ /** Enclosing loop's own scope, for a NESTED loop (#2861) — see `buildLoopRowScope`. */
1516
+ parentScope?: BindingScope,
1428
1517
  ): LoopChildConditional[] {
1429
1518
  const conditionals: LoopChildConditional[] = []
1430
- const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
1519
+ const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex, parentScope)
1431
1520
 
1432
1521
  // Widen the source-level "references loop param" check so destructured
1433
1522
  // callbacks fire too — the pattern text `[, cfg]` never word-matches on
1434
1523
  // bare `cfg` but individual binding names do (#951). Consumes a
1435
1524
  // pre-computed `Set<string>` of free identifiers (#1267) rather than
1436
1525
  // running word-boundary regex on the expression text.
1437
- const refsAnyBindingViaFreeIds = (freeIds: ReadonlySet<string>): boolean => {
1438
- if (loopParamBindings && loopParamBindings.length > 0) {
1439
- for (const b of loopParamBindings) {
1440
- if (freeIds.has(b.name)) return true
1441
- }
1442
- return false
1443
- }
1444
- return loopParam ? freeIds.has(loopParam) : false
1445
- }
1526
+ //
1527
+ // #2861: also checks the loop's INDEX binding, not just item/destructure
1528
+ // — `scope.valueBoundNames()` already unions all three (the same query
1529
+ // Phase 1's `referencesLoopParam` reads to grant a slot at all), so a
1530
+ // condition referencing only the index (e.g. `{i % 2 === 0 ? … : null}`)
1531
+ // no longer dies at this pre-gate before `classifyReactivity` even runs.
1532
+ const refsAnyBindingViaFreeIds = (freeIds: ReadonlySet<string>): boolean =>
1533
+ scope !== undefined && anyNameIn(scope.valueBoundNames(), freeIds)
1446
1534
 
1447
1535
  walkIR(node, null, {
1448
1536
  // element / fragment / component / provider auto-descend with same scope.
@@ -1473,13 +1561,13 @@ export function collectLoopChildConditionals(
1473
1561
  preambleNames !== undefined &&
1474
1562
  preambleNames.size > 0 &&
1475
1563
  anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames)
1476
- // Loop-param conditionals are reactive via per-item signal accessors;
1477
- // classifyReactivity sees both paths (signal/memo/prop + loop-param).
1478
- if (!readsPreamble && classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
1564
+ // Loop-param/loop-index conditionals are reactive via per-item signal
1565
+ // accessors; classifyReactivity sees all three paths (signal/memo/prop
1566
+ // + loop-param + loop-index, #2861).
1567
+ if (!readsPreamble && classifyReactivity(expanded.expr, ctx, scope, expanded.freeIds).kind === 'none') return
1479
1568
 
1480
- const loopParamsForCond = loopParam
1481
- ? [{ param: loopParam, bindings: loopParamBindings }]
1482
- : undefined
1569
+ const loopParamsForCond =
1570
+ loopParams ?? (loopParam ? [{ param: loopParam, bindings: loopParamBindings, index: loopIndex }] : undefined)
1483
1571
  // `__slots` matches the closure variable emitted by
1484
1572
  // `stringifyLoopChildConditional` — Child-position interpolations
1485
1573
  // get wrapped in `__bfSlot(EXPR, __slots)` so live `Node` returns
@@ -1520,7 +1608,13 @@ function summarizeLoopChildBranch(
1520
1608
  /** Enclosing loop's index param name — see `collectLoopChildConditionals` (Copilot review on #2595). */
1521
1609
  loopIndex?: string | null,
1522
1610
  ): LoopChildBranchSummary {
1523
- const inner = collectInnerLoops([node], siblingOffsets, loopParam, ctx, branchInnerLoopOptions)
1611
+ const inner = collectInnerLoops(
1612
+ [node],
1613
+ siblingOffsets,
1614
+ loopParam ? { param: loopParam, bindings: loopParamBindings, index: loopIndex } : undefined,
1615
+ ctx,
1616
+ branchInnerLoopOptions,
1617
+ )
1524
1618
  return {
1525
1619
  childComponents: collectConditionalBranchChildComponents(node),
1526
1620
  innerLoops: inner.length > 0 ? inner : undefined,
@@ -11,14 +11,13 @@
11
11
  */
12
12
 
13
13
  import type { BranchLoop } from '../../types.ts'
14
- import { buildChainedArrayExpr, varSlotId, wrapLoopParamAsAccessor } from '../../utils.ts'
14
+ import { buildChainedArrayExpr, varSlotId } from '../../utils.ts'
15
15
  import { buildBranchCompositePlan } from './build-composite-loop.ts'
16
16
  import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
17
17
  import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
18
- import { buildLazyRowPlan } from './build-lazy-row.ts'
18
+ import { buildPlainRowCore } from './build-plain-row.ts'
19
19
  import type { LazyRowScopeInfo } from './lazy-row-eligibility.ts'
20
- import { destructureLoopParam, loopKeyFn, buildChildRefBindings, buildPreambleRegionPlans } from '../shared.ts'
21
- import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
20
+ import { loopKeyFn, buildChildRefBindings } from '../shared.ts'
22
21
  import type {
23
22
  BranchCompositeLoopPlan,
24
23
  BranchLoopPlan,
@@ -44,7 +43,6 @@ export function buildBranchLoopPlan(
44
43
  return composite
45
44
  }
46
45
 
47
- const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
48
46
  const hasReactiveEffects = loop.bindings.reactiveAttrs.length > 0
49
47
  || loop.bindings.reactiveTexts.length > 0
50
48
  || loop.bindings.conditionals.length > 0
@@ -54,14 +52,18 @@ export function buildBranchLoopPlan(
54
52
  const arrayExpr = fm
55
53
  ? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
56
54
  : buildChainedArrayExpr(loop)
57
- const indexParam = loop.index || '__idx'
58
- const mapPreambleWrapped = loop.preamble
59
- ? renderPreamble(loop.preamble, {
60
- transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
61
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
62
- })
63
- : ''
64
- const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings)
55
+
56
+ // Shared "wrap item → decide lazy → wrap index" core (#2859 follow-up) —
57
+ // see `build-plain-row.ts`'s module docstring for the full rationale.
58
+ const core = buildPlainRowCore({
59
+ loop,
60
+ arrayExpr,
61
+ callSite: 'branch-plain',
62
+ flatMapLeafItem: Boolean(fm),
63
+ anchored: false,
64
+ scope: lazyScope,
65
+ })
66
+ const { paramHead, paramUnwrap, indexParam, preambleRegions, lazyRow } = core
65
67
  const plan: BranchPlainLoopPlan = {
66
68
  kind: 'plain',
67
69
  rowConstruction: 'string-template',
@@ -78,21 +80,10 @@ export function buildBranchLoopPlan(
78
80
  indexParam,
79
81
  // Wrap loop-param references to signal-accessor form so the preamble
80
82
  // matches the template literal's already-wrapped reads (#1065).
81
- mapPreambleWrapped,
83
+ mapPreambleWrapped: core.mapPreambleWrapped,
82
84
  // Lazy row graph (§9, L3) — undefined for every ineligible loop.
83
- lazyRow: buildLazyRowPlan({
84
- loop,
85
- arrayExpr,
86
- indexParam,
87
- paramUnwrap,
88
- mapPreambleWrapped,
89
- preambleRegionCount: preambleRegions.length,
90
- callSite: 'branch-plain',
91
- flatMapLeafItem: Boolean(fm),
92
- anchored: false,
93
- scope: lazyScope,
94
- }) ?? undefined,
95
- template: loop.template,
85
+ lazyRow,
86
+ template: core.template,
96
87
  reactiveEffects: hasReactiveEffects
97
88
  ? buildReactiveEffectsPlan({
98
89
  attrs: loop.bindings.reactiveAttrs,
@@ -100,11 +91,12 @@ export function buildBranchLoopPlan(
100
91
  conditionals: loop.bindings.conditionals,
101
92
  loopParam: loop.param,
102
93
  loopParamBindings: loop.paramBindings,
94
+ loopIndex: loop.index,
103
95
  profileComponentName,
104
96
  })
105
97
  : null,
106
98
  eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
107
- childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
99
+ childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
108
100
  preambleRegions,
109
101
  bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
110
102
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
@@ -34,8 +34,8 @@ import { internalInvariant } from '../../../errors.ts'
34
34
  /** @internal — prefer `buildLoopPlan`. */
35
35
  export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?: string): ComponentLoopPlan {
36
36
  const { name } = elem.childComponent!
37
- const propsExpr = buildComponentPropsExpr(elem.childComponent!, elem.param)
38
- const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings)
37
+ const propsExpr = buildComponentPropsExpr(elem.childComponent!, elem.param, undefined, elem.index)
38
+ const keyExpr = wrapLoopParamAsAccessor(elem.key || '__idx', elem.param, elem.paramBindings, elem.index)
39
39
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
40
40
 
41
41
  // A component-root loop's preamble is JS-only by construction: Phase 1
@@ -47,7 +47,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
47
47
  // that let a JSX-bearing preamble through is missing, not this line.
48
48
  const mapPreambleWrapped = elem.preamble
49
49
  ? renderPreamble(elem.preamble, {
50
- transformJs: text => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings),
50
+ transformJs: text => wrapLoopParamAsAccessor(text, elem.param, elem.paramBindings, elem.index),
51
51
  renderLeaf: () => {
52
52
  internalInvariant(false, 'component-root loop received a JSX-bearing preamble — Phase 1 should have refused it')
53
53
  },
@@ -62,13 +62,14 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
62
62
  : false
63
63
  const rawChildrenExpr = isTextOnly ? irChildrenToJsExpr(comp.children!) : null
64
64
  const childrenFreeIds = isTextOnly && comp.children ? irChildrenFreeIds(comp.children) : undefined
65
- const childrenRefsLoop = rawChildrenExpr != null && childrenFreeIds != null && childrenFreeIds.has(elem.param)
65
+ const childrenRefsLoop = rawChildrenExpr != null && childrenFreeIds != null
66
+ && (childrenFreeIds.has(elem.param) || (!!elem.index && childrenFreeIds.has(elem.index)))
66
67
  return {
67
68
  componentName: comp.name,
68
69
  selector: buildCompSelector(comp),
69
- propsExpr: buildComponentPropsExpr(comp, elem.param),
70
+ propsExpr: buildComponentPropsExpr(comp, elem.param, undefined, elem.index),
70
71
  childrenTextEffect: childrenRefsLoop
71
- ? { wrappedChildren: wrapLoopParamAsAccessor(rawChildrenExpr!, elem.param, elem.paramBindings) }
72
+ ? { wrappedChildren: wrapLoopParamAsAccessor(rawChildrenExpr!, elem.param, elem.paramBindings, elem.index) }
72
73
  : null,
73
74
  }
74
75
  })
@@ -98,7 +99,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
98
99
  // the per-item factory has no `__el` handle to invoke. Still required
99
100
  // by the type so the structural invariant (every variant has a
100
101
  // `childRefs`) is preserved; populated as empty.
101
- childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
102
+ childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
102
103
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
103
104
  childConditionalEffects: hasChildConds
104
105
  ? buildReactiveEffectsPlan({
@@ -107,6 +108,7 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
107
108
  conditionals: elem.bindings.conditionals,
108
109
  loopParam: elem.param,
109
110
  loopParamBindings: elem.paramBindings,
111
+ loopIndex: elem.index,
110
112
  profileComponentName,
111
113
  })
112
114
  : null,
@@ -31,7 +31,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
31
31
  const nestedComps = elem.nestedComponents!
32
32
  const depthLevels = buildDepthLevels(elem.innerLoops ?? [], nestedComps, elem.bindings.events)
33
33
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(elem.param, elem.paramBindings)
34
- const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings)
34
+ const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, elem.param, elem.paramBindings, elem.index)
35
35
 
36
36
  const outerCompsByDepth = nestedComps.filter(c => !c.loopDepth || c.loopDepth === 0)
37
37
 
@@ -48,21 +48,23 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
48
48
  mapPreambleWrapped: elem.preamble
49
49
  ? renderPreamble(elem.preamble, {
50
50
  transformJs: wrap,
51
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
51
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings, index: elem.index }], undefined),
52
52
  })
53
53
  : '',
54
54
  template: elem.template,
55
55
  outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
56
56
  outerEvents: elem.bindings.events.filter(ev => ev.nestedLoops.length === 0),
57
- childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
57
+ childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings, elem.index),
58
58
  innerLoops: buildInnerLoopsPlan({
59
59
  levels: depthLevels,
60
60
  parentElVar: '__el',
61
61
  outerLoopParam: elem.param,
62
62
  outerLoopParamBindings: elem.paramBindings,
63
+ outerLoopIndex: elem.index,
63
64
  }),
64
65
  loopParam: elem.param,
65
66
  loopParamBindings: elem.paramBindings,
67
+ loopIndex: elem.index,
66
68
  reactiveEffects: hasReactive(elem)
67
69
  ? buildReactiveEffectsPlan({
68
70
  attrs: elem.bindings.reactiveAttrs,
@@ -70,6 +72,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
70
72
  conditionals: elem.bindings.conditionals,
71
73
  loopParam: elem.param,
72
74
  loopParamBindings: elem.paramBindings,
75
+ loopIndex: elem.index,
73
76
  profileComponentName,
74
77
  })
75
78
  : null,
@@ -88,7 +91,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
88
91
  const childEvents = loop.bindings.events
89
92
  const depthLevels = buildDepthLevels(innerLoops, nestedComps, childEvents)
90
93
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(loop.param, loop.paramBindings)
91
- const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings)
94
+ const wrap = (expr: string) => wrapLoopParamAsAccessor(expr, loop.param, loop.paramBindings, loop.index)
92
95
 
93
96
  const outerCompsByDepth = nestedComps.filter(c => !c.loopDepth || c.loopDepth === 0)
94
97
 
@@ -108,21 +111,23 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
108
111
  mapPreambleWrapped: loop.preamble
109
112
  ? renderPreamble(loop.preamble, {
110
113
  transformJs: wrap,
111
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
114
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings, index: loop.index }], undefined),
112
115
  })
113
116
  : '',
114
117
  template: loop.template,
115
118
  outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
116
119
  outerEvents: childEvents.filter(ev => ev.nestedLoops.length === 0),
117
- childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
120
+ childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings, loop.index),
118
121
  innerLoops: buildInnerLoopsPlan({
119
122
  levels: depthLevels,
120
123
  parentElVar: '__el',
121
124
  outerLoopParam: loop.param,
122
125
  outerLoopParamBindings: loop.paramBindings,
126
+ outerLoopIndex: loop.index,
123
127
  }),
124
128
  loopParam: loop.param,
125
129
  loopParamBindings: loop.paramBindings,
130
+ loopIndex: loop.index,
126
131
  reactiveEffects: hasReactiveBranch(loop)
127
132
  ? buildReactiveEffectsPlan({
128
133
  attrs: loop.bindings.reactiveAttrs,
@@ -130,6 +135,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
130
135
  conditionals: loop.bindings.conditionals,
131
136
  loopParam: loop.param,
132
137
  loopParamBindings: loop.paramBindings,
138
+ loopIndex: loop.index,
133
139
  profileComponentName,
134
140
  })
135
141
  : null,