@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
@@ -441,7 +441,7 @@ export function initExample(__scope, _p = {}) {
441
441
 
442
442
  mapArray(() => (todos()).flatMap((todo) => {
443
443
  if (todo.hidden) return []
444
- return todo.tags.map(tag => ({ k: (\`\${todo.id}:\${tag}\`), h: \`<li>\${escapeText((tag))}</li>\` }))
444
+ return todo.tags.map(tag => ({ k: (\`\${todo.id}:\${tag}\`), h: \`<li>\${escapeText(tag)}</li>\` }))
445
445
  }), _s0, (__bfD, __bfI) => String(__bfD.k ?? __bfI), (__bfD, __idx, __existing) => {
446
446
  let __el = __existing
447
447
  if (!__el) { const __tpl = document.createElement('template'); __tpl.innerHTML = __bfD().h; __el = __tpl.content.firstElementChild }
@@ -458,7 +458,7 @@ export function initExample(__scope, _p = {}) {
458
458
 
459
459
  hydrate('Example', { init: initExample, template: (_p) => \`<div bf="s0"><!--bf-loop:l0-->\${([]).flatMap((todo) => {
460
460
  if (todo.hidden) return []
461
- return todo.tags.map(tag => \`<li>\${escapeText((tag))}</li>\`)
461
+ return todo.tags.map(tag => \`<li>\${escapeText(tag)}</li>\`)
462
462
  }).join('')}<!--bf-/loop:l0--></div>\` })
463
463
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
464
464
  `;
@@ -2550,6 +2550,99 @@ hydrate('StatementExample', { init: initStatementExample, template: (_p) => \`<d
2550
2550
  export function StatementExample(_p, __bfKey) { return createComponent('StatementExample', _p, __bfKey) }"
2551
2551
  `;
2552
2552
 
2553
+ exports[`docs/core/reactivity/untrack.md doc-examples L79 — (no label) 1`] = `
2554
+ "import { $, createComponent, createSignal, escapeAttr, escapeText, hydrate, lazySlots, mapArrayLazy, qsa, textOrNode, untrack } from '@barefootjs/client/runtime'
2555
+
2556
+ var renderPreview = renderPreview ?? function(id) {
2557
+ return \`<p>Preview for item \${id}</p>\`
2558
+ }
2559
+
2560
+ export function initGallery(__scope, _p = {}) {
2561
+ if (!__scope) return
2562
+ const __scopeId = __scope.getAttribute('bf-s')
2563
+
2564
+ const [items, setItems] = createSignal([{ id: 1, label: 'First' }, { id: 2, label: 'Second' }])
2565
+ const rename = () => setItems(items().map(item => ({ id: item.id, label: item.label + '!' })))
2566
+
2567
+ const [_s0, _s3] = $(__scope, 's0', 's3')
2568
+
2569
+ if (_s0) _s0.addEventListener('click', rename)
2570
+ const __tpl_l0 = document.createElement('template')
2571
+ __tpl_l0.innerHTML = \`<li data-key="" bf="s2"><!--bf:s1--><!--/--></li>\`
2572
+ const __lzp_l0 = [[0]]
2573
+ const __lzs_l0 = [{ id: 's1', kind: 'text', path: [] }]
2574
+ const __lzsc_l0 = [{ id: 's1', kind: 'text', path: __lzp_l0[0] }]
2575
+ mapArrayLazy(() => items(), _s3, (item) => String(item.id), {
2576
+ createRow: (__e, __idx) => {
2577
+ const item = () => __e.item
2578
+ const __el = __tpl_l0.content.firstElementChild.cloneNode(true)
2579
+ const __r = __e.refs = [__el, lazySlots(__el, __lzsc_l0)]
2580
+ const __l = __e.last = []
2581
+ { const __t = __r[0]
2582
+ if (__t) {
2583
+ { const __x = item().label
2584
+ { const __v = __x; if (__v != null) __t.setAttribute('title', String(__v)); else __t.removeAttribute('title') }
2585
+ __l[0] = __x }
2586
+ } }
2587
+ { const __t = __r[0]
2588
+ if (__t) {
2589
+ { const __x = untrack(() => renderPreview(item().id))
2590
+ { const __v = __x; if (__v != null) __t.setAttribute('data-preview', String(__v)); else __t.removeAttribute('data-preview') }
2591
+ __l[1] = __x }
2592
+ } }
2593
+ { const __x = item().label
2594
+ __r[1]('s1', textOrNode(__x))
2595
+ __l[2] = __x }
2596
+ return __el
2597
+ },
2598
+ applyItem: (__e) => {
2599
+ const item = () => __e.item
2600
+ const __r = __e.refs ?? (__e.refs = [])
2601
+ const __l = __e.last ?? (__e.last = [])
2602
+ { const __t = 0 in __r ? __r[0] : (__r[0] = qsa(__e.primaryEl, '[bf="s2"]'))
2603
+ if (__t) {
2604
+ { const __x = item().label
2605
+ if (!(0 in __l) || !Object.is(__l[0], __x)) {
2606
+ { const __v = __x; if (__v != null) __t.setAttribute('title', String(__v)); else __t.removeAttribute('title') }
2607
+ }
2608
+ __l[0] = __x }
2609
+ } }
2610
+ { const __t = 0 in __r ? __r[0] : (__r[0] = qsa(__e.primaryEl, '[bf="s2"]'))
2611
+ if (__t) {
2612
+ { const __x = untrack(() => renderPreview(item().id))
2613
+ if (!(1 in __l) || !Object.is(__l[1], __x)) {
2614
+ { const __v = __x; if (__v != null) __t.setAttribute('data-preview', String(__v)); else __t.removeAttribute('data-preview') }
2615
+ }
2616
+ __l[1] = __x }
2617
+ } }
2618
+ const __d = __r[1] ?? (__r[1] = lazySlots(__e.primaryEl, __lzs_l0))
2619
+ { const __x = item().label
2620
+ if (!(2 in __l) || !Object.is(__l[2], __x)) __d('s1', textOrNode(__x))
2621
+ __l[2] = __x }
2622
+ },
2623
+ applyOuter: (__es, __seed) => {
2624
+ for (const __e of __es) {
2625
+ const item = () => __e.item
2626
+ const __r = __e.refs ?? (__e.refs = [])
2627
+ const __l = __e.last ?? (__e.last = [])
2628
+ { const __t = 0 in __r ? __r[0] : (__r[0] = qsa(__e.primaryEl, '[bf="s2"]'))
2629
+ if (__t) {
2630
+ { const __x = untrack(() => renderPreview(item().id))
2631
+ if (__seed ? (__t.getAttribute('data-preview') !== (__x != null ? String(__x) : null)) : (!(1 in __l) || !Object.is(__l[1], __x))) {
2632
+ { const __v = __x; if (__v != null) __t.setAttribute('data-preview', String(__v)); else __t.removeAttribute('data-preview') }
2633
+ }
2634
+ __l[1] = __x }
2635
+ } }
2636
+ }
2637
+ },
2638
+ }, 'l0')
2639
+
2640
+ }
2641
+
2642
+ hydrate('Gallery', { init: initGallery, template: (_p) => \`<div><button bf="s0">Rename all</button><ul bf="s3"><!--bf-loop:l0-->\${([{ id: 1, label: 'First' }, { id: 2, label: 'Second' }]).map((item) => \`<li data-key="\${escapeAttr(item.id)}" \${(item.label) != null ? 'title="' + escapeAttr(item.label) + '"' : ''} \${(untrack(() => renderPreview(item.id))) != null ? 'data-preview="' + escapeAttr(untrack(() => renderPreview(item.id))) + '"' : ''} bf="s2"><!--bf:s1-->\${escapeText(item.label)}<!--/--></li>\`).join('')}<!--bf-/loop:l0--></ul></div>\` })
2643
+ export function Gallery(_p, __bfKey) { return createComponent('Gallery', _p, __bfKey) }"
2644
+ `;
2645
+
2553
2646
  exports[`docs/core/reactivity/props-reactivity.md doc-examples L16 — (no label) 1`] = `
2554
2647
  "import { createComponent, hydrate } from '@barefootjs/client/runtime'
2555
2648
 
@@ -2635,12 +2728,13 @@ export function initExample(__scope, _p = {}) {
2635
2728
  })
2636
2729
 
2637
2730
  // Reactive child component props
2731
+ { const __l = []
2638
2732
  createEffect(() => {
2639
2733
  const [__Child_s0El] = $c(__scope, 's0')
2640
2734
  if (__Child_s0El) {
2641
2735
  if ('value' in __Child_s0El) { const __val = String(count()); if (__Child_s0El.value !== __val) __Child_s0El.value = __val }
2642
2736
  }
2643
- })
2737
+ }) }
2644
2738
 
2645
2739
  // Initialize child components with props
2646
2740
  initChild('Child', _s0, { get value() { return count() } })
@@ -2651,7 +2745,7 @@ export function Example(_p, __bfKey) { return createComponent('Example', _p, __b
2651
2745
  `;
2652
2746
 
2653
2747
  exports[`docs/core/reactivity/props-reactivity.md doc-examples L85 — Compiled props object 1`] = `
2654
- "import { createComponent, createEffect, createSignal, escapeTextOrMarkup, escapeTextOrNode, hydrate, lazySlots } from '@barefootjs/client/runtime'
2748
+ "import { createComponent, createEffect, createSignal, escapeTextOrMarkup, escapeTextOrNode, hydrate, lazySlots, escapeText } from '@barefootjs/client/runtime'
2655
2749
 
2656
2750
  export function initTodoItem(__scope, _p = {}) {
2657
2751
  if (!__scope) return
@@ -2688,8 +2782,6 @@ export function Dashboard(_p, __bfKey) { return createComponent('Dashboard__b3c3
2688
2782
  export function initExample(__scope, _p = {}) {
2689
2783
  if (!__scope) return
2690
2784
  const __scopeId = __scope.getAttribute('bf-s')
2691
-
2692
- const children = _p.children
2693
2785
  const [count, setCount] = createSignal(0)
2694
2786
  const [isLoggedIn] = createSignal(false)
2695
2787
  const [todos] = createSignal([])
@@ -2700,7 +2792,7 @@ export function initExample(__scope, _p = {}) {
2700
2792
 
2701
2793
  }
2702
2794
 
2703
- hydrate('Example', { init: initExample, template: (_p) => \`<div>\${get} value() \${} return count() } } </div>\` })
2795
+ hydrate('Example', { init: initExample, template: (_p) => \`<div>\${escapeText(get)} value() \${escapeText()} return count() } } </div>\` })
2704
2796
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
2705
2797
  `;
2706
2798
 
@@ -2752,7 +2844,7 @@ export function Counter(_p, __bfKey) { return createComponent('Counter', _p, __b
2752
2844
  `;
2753
2845
 
2754
2846
  exports[`docs/core/components/component-authoring.mdx doc-examples L79 — (no label) 1`] = `
2755
- "import { $, __bfSlot, createComponent, createSignal, hydrate, insert } from '@barefootjs/client/runtime'
2847
+ "import { $, __bfSlot, createComponent, createSignal, escapeText, hydrate, insert } from '@barefootjs/client/runtime'
2756
2848
 
2757
2849
 
2758
2850
  export function initToggle(__scope, _p = {}) {
@@ -2776,7 +2868,7 @@ export function initToggle(__scope, _p = {}) {
2776
2868
  if (_s1) _s1.addEventListener('click', () => { setOn(prev => !prev) })
2777
2869
  }
2778
2870
 
2779
- hydrate('Toggle', { init: initToggle, template: (_p) => \`<button bf="s1">\${(false) ? \`<!--bf-cond-start:s0-->\${'ON'}<!--bf-cond-end:s0-->\` : \`<!--bf-cond-start:s0-->\${'OFF'}<!--bf-cond-end:s0-->\`}</button>\` })
2871
+ hydrate('Toggle', { init: initToggle, template: (_p) => \`<button bf="s1">\${(false) ? \`<!--bf-cond-start:s0-->\${escapeText('ON')}<!--bf-cond-end:s0-->\` : \`<!--bf-cond-start:s0-->\${escapeText('OFF')}<!--bf-cond-end:s0-->\`}</button>\` })
2780
2872
  export function Toggle(_p, __bfKey) { return createComponent('Toggle', _p, __bfKey) }"
2781
2873
  `;
2782
2874
 
@@ -3255,12 +3347,17 @@ export function initExample(__scope, _p = {}) {
3255
3347
 
3256
3348
 
3257
3349
  // Reactive child component props
3350
+ { const __l = []
3258
3351
  createEffect(() => {
3259
3352
  const [__Dialog_s9El] = $c(__scope, 's9')
3260
3353
  if (__Dialog_s9El) {
3261
- __Dialog_s9El.open = !!(open())
3354
+ { const __x = open()
3355
+ if (!(0 in __l) || !Object.is(__l[0], __x)) {
3356
+ __Dialog_s9El.open = !!(__x)
3357
+ }
3358
+ __l[0] = __x }
3262
3359
  }
3263
- })
3360
+ }) }
3264
3361
 
3265
3362
  // Initialize child components with props
3266
3363
  initChild('Dialog', _s9, { get open() { return open() }, onOpenChange: setOpen })
@@ -3823,7 +3920,7 @@ hydrate('StatementExample', { init: initStatementExample, template: (_p) => \`<d
3823
3920
  export function StatementExample(_p, __bfKey) { return createComponent('StatementExample', _p, __bfKey) }"
3824
3921
  `;
3825
3922
 
3826
- exports[`docs/core/components/portals.md doc-examples L134 — (no label) 1`] = `
3923
+ exports[`docs/core/components/portals.md doc-examples L141 — (no label) 1`] = `
3827
3924
  "import { $, createComponent, createEffect, createPortal, hydrate, isSSRPortal, useContext } from '@barefootjs/client/runtime'
3828
3925
 
3829
3926
 
@@ -3862,7 +3959,7 @@ hydrate('DialogOverlay__b3c36eee', { init: initDialogOverlay, template: (_p) =>
3862
3959
  export function DialogOverlay(_p, __bfKey) { return createComponent('DialogOverlay__b3c36eee', _p, __bfKey) }"
3863
3960
  `;
3864
3961
 
3865
- exports[`docs/core/components/portals.md doc-examples L190 — (no label) 1`] = `
3962
+ exports[`docs/core/components/portals.md doc-examples L197 — (no label) 1`] = `
3866
3963
  "import { createComponent, createEffect, createMemo, createPortal, createSignal, hydrate, onCleanup, onMount, untrack } from '@barefootjs/client/runtime'
3867
3964
 
3868
3965
 
@@ -3938,11 +4035,16 @@ export function initButton(__scope, _p = {}) {
3938
4035
 
3939
4036
  const [_s0] = $(__scope, 's0')
3940
4037
 
4038
+ { const __l = []
3941
4039
  createEffect(() => {
3942
4040
  if (_s0) {
3943
- { const __v = classes; if (__v != null) _s0.setAttribute('class', String(__v)); else _s0.removeAttribute('class') }
4041
+ { const __x = classes
4042
+ if (!(0 in __l) || !Object.is(__l[0], __x)) {
4043
+ { const __v = __x; if (__v != null) _s0.setAttribute('class', String(__v)); else _s0.removeAttribute('class') }
4044
+ }
4045
+ __l[0] = __x }
3944
4046
  }
3945
- })
4047
+ }) }
3946
4048
 
3947
4049
  if (_s0) applyRestAttrs(_s0, _p, ["class"])
3948
4050
 
@@ -4111,24 +4213,38 @@ export function initProductPage(__scope, _p = {}) {
4111
4213
  __bfw_s2('s2', escapeTextOrNode(__val))
4112
4214
  })
4113
4215
 
4216
+ { const __l = []
4114
4217
  createEffect(() => {
4115
4218
  if (_s4) {
4116
- { const __v = _p.product.image; if (__v != null) _s4.setAttribute('src', String(__v)); else _s4.removeAttribute('src') }
4219
+ { const __x = _p.product.image
4220
+ if (!(0 in __l) || !Object.is(__l[0], __x)) {
4221
+ { const __v = __x; if (__v != null) _s4.setAttribute('src', String(__v)); else _s4.removeAttribute('src') }
4222
+ }
4223
+ __l[0] = __x }
4117
4224
  }
4118
- })
4225
+ }) }
4119
4226
 
4120
4227
 
4121
4228
  // Reactive child component props
4229
+ { const __l = []
4122
4230
  createEffect(() => {
4123
4231
  const [__ReviewStars_s5El] = $c(__scope, 's5')
4124
4232
  if (__ReviewStars_s5El) {
4125
- { const __v = product.rating; if (__v != null) __ReviewStars_s5El.setAttribute('rating', String(__v)); else __ReviewStars_s5El.removeAttribute('rating') }
4233
+ { const __x = product.rating
4234
+ if (!(0 in __l) || !Object.is(__l[0], __x)) {
4235
+ { const __v = __x; if (__v != null) __ReviewStars_s5El.setAttribute('rating', String(__v)); else __ReviewStars_s5El.removeAttribute('rating') }
4236
+ }
4237
+ __l[0] = __x }
4126
4238
  }
4127
4239
  const [__AddToCart_s6El] = $c(__scope, 's6')
4128
4240
  if (__AddToCart_s6El) {
4129
- { const __v = product.id; if (__v != null) __AddToCart_s6El.setAttribute('productId', String(__v)); else __AddToCart_s6El.removeAttribute('productId') }
4241
+ { const __x = product.id
4242
+ if (!(1 in __l) || !Object.is(__l[1], __x)) {
4243
+ { const __v = __x; if (__v != null) __AddToCart_s6El.setAttribute('productId', String(__v)); else __AddToCart_s6El.removeAttribute('productId') }
4244
+ }
4245
+ __l[1] = __x }
4130
4246
  }
4131
- })
4247
+ }) }
4132
4248
 
4133
4249
  // Initialize child components with props
4134
4250
  initChild('ReviewStars', _s5, { get rating() { return product.rating } })
@@ -4337,7 +4453,7 @@ export function Example(_p, __bfKey) { return createComponent('Example', _p, __b
4337
4453
  `;
4338
4454
 
4339
4455
  exports[`docs/core/adapters/hono-adapter.md doc-examples L177 — (no label) 1`] = `
4340
- "import { createComponent, createEffect, createSignal, escapeTextOrMarkup, escapeTextOrNode, hydrate, lazySlots, $, __bfSlot, insert } from '@barefootjs/client/runtime'
4456
+ "import { createComponent, createEffect, createSignal, escapeTextOrMarkup, escapeTextOrNode, hydrate, lazySlots, $, __bfSlot, escapeText, insert } from '@barefootjs/client/runtime'
4341
4457
 
4342
4458
  export function initTodoItem(__scope, _p = {}) {
4343
4459
  if (!__scope) return
@@ -4396,7 +4512,7 @@ export function initExample(__scope, _p = {}) {
4396
4512
 
4397
4513
  }
4398
4514
 
4399
- hydrate('Example', { init: initExample, template: (_p) => \`<div>\${on() ? \`<!--bf-cond-start:s0-->\${'ON'}<!--bf-cond-end:s0-->\` : \`<!--bf-cond-start:s0-->\${'OFF'}<!--bf-cond-end:s0-->\`}</div>\` })
4515
+ hydrate('Example', { init: initExample, template: (_p) => \`<div>\${on() ? \`<!--bf-cond-start:s0-->\${escapeText('ON')}<!--bf-cond-end:s0-->\` : \`<!--bf-cond-start:s0-->\${escapeText('OFF')}<!--bf-cond-end:s0-->\`}</div>\` })
4400
4516
  export function Example(_p, __bfKey) { return createComponent('Example', _p, __bfKey) }"
4401
4517
  `;
4402
4518
 
@@ -192,8 +192,16 @@ const ALLOWLIST: Record<string, Partial<Record<Pattern, number>>> = {
192
192
  },
193
193
  // FLOOR (shape 3): `irToPlaceholderTemplate`'s loop-param accessor-rewrite
194
194
  // spec forwarding — see the canonical docstring on
195
- // `wrapExprWithLoopParams` in `ir-to-client-js/utils.ts`.
196
- 'packages/jsx/src/ir-to-client-js/collect-elements.ts': { loopParams: 9 },
195
+ // `wrapExprWithLoopParams` in `ir-to-client-js/utils.ts`. Grew by 3 for
196
+ // #2868: `collectLoopChildConditionals` gained an optional `loopParams`
197
+ // parameter (the full ancestor loop-param chain) so a conditional arm
198
+ // inside an inner `.map()` can render against an outer loop's item/index
199
+ // too — same accessor-rewrite-payload shape, one more forwarding site.
200
+ // Grew by 1 more merging #2861's `parentScope` param (a DIFFERENT,
201
+ // independent extension of the same function for the reactivity-scope
202
+ // concern) alongside #2868's `loopParams` — both trailing params now
203
+ // coexist on `collectLoopChildConditionals`'s signature.
204
+ 'packages/jsx/src/ir-to-client-js/collect-elements.ts': { loopParams: 13 },
197
205
  // FLOOR (shape 2): `computeCsrInlinability`'s fixed-point constant-chain
198
206
  // inlining loop over `ctx.localConstants` — component-scope const
199
207
  // resolution, not loop-row scope.
@@ -202,6 +210,16 @@ const ALLOWLIST: Record<string, Partial<Record<Pattern, number>>> = {
202
210
  // pass a loop-param accessor-rewrite spec (see the file's own comments) —
203
211
  // the remaining mentions are all in that reasoning, not device usage.
204
212
  'packages/jsx/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts': { loopParams: 4 },
213
+ // FLOOR (shape 3, new for #2868): a doc comment pointing at
214
+ // `collect-elements.ts`'s `irToHtmlTemplate` `loopParams` — explaining why
215
+ // `cond.whenTrueHtml`/`whenFalseHtml` must not be re-wrapped post-hoc.
216
+ 'packages/jsx/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts': { loopParams: 1 },
217
+ // FLOOR (shape 3, new for #2868): same doc-comment reference as
218
+ // `build-lazy-row.ts` above.
219
+ 'packages/jsx/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts': { loopParams: 1 },
220
+ // FLOOR (shape 3, new for #2868): same doc-comment reference as
221
+ // `build-lazy-row.ts` above.
222
+ 'packages/jsx/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts': { loopParams: 1 },
205
223
  // FLOOR (shape 3): `irToHtmlTemplate`/`irToPlaceholderTemplate`'s
206
224
  // loop-param accessor-rewrite spec — see `ir-to-client-js/utils.ts`'s
207
225
  // `wrapExprWithLoopParams` docstring (the canonical explanation, pointed
@@ -221,6 +239,11 @@ const ALLOWLIST: Record<string, Partial<Record<Pattern, number>>> = {
221
239
  // `loopParams`-named parameter in `ir-to-client-js/` forwards. See its
222
240
  // docstring (added Stage 4) for the full shape-3 reasoning.
223
241
  'packages/jsx/src/ir-to-client-js/utils.ts': { loopParams: 4 },
242
+ // FLOOR (shape 3, new for #2868): `TopLevelLoop.templateIndexed` /
243
+ // `BranchLoop.templateIndexed`'s doc comments point at `irToHtmlTemplate`'s
244
+ // `loopParams` to explain why the index-wrapped render is a second
245
+ // structured render, not a post-hoc regex pass over `template`.
246
+ 'packages/jsx/src/ir-to-client-js/types.ts': { loopParams: 1 },
224
247
  // `jsx-to-ir.ts` — MIGRATED to 0 in Stage 4: `makeBindingEnv`'s
225
248
  // `loopParams: boundNames` field now reads `loopValueBoundNames:
226
249
  // boundNames`, matching `free-refs.ts`'s renamed field. No entry needed
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Regression pin for the `buildPlainRowCore` extraction
3
+ * (`control-flow/plan/build-plain-row.ts`).
4
+ *
5
+ * `buildPlainLoopPlan` (build-loop.ts, `callSite: 'plain'`) and
6
+ * `buildBranchLoopPlan` (build-branch-loop.ts, `callSite: 'branch-plain'`)
7
+ * used to duplicate the "wrap item → decide lazy → wrap index" sequence
8
+ * verbatim. This pins that both call sites now go through the ONE shared
9
+ * implementation and cannot silently diverge again (CLAUDE.md's "one
10
+ * decision, two implementations, no test comparing them"):
11
+ *
12
+ * - A row forced eager for a reason UNRELATED to the index (here, an
13
+ * imperative ref — #2859 follow-on lifted the index-only refusal, so a
14
+ * bare index reference no longer forces eager on its own) still gets
15
+ * its index wrapped into a live accessor call at BOTH call sites.
16
+ * - A lazy-eligible row (index-referencing or not) goes through
17
+ * `mapArrayLazy` at BOTH call sites, with no leaked eager-only index
18
+ * wrap in the shared `mapPreambleWrapped`/`template` strings — the
19
+ * exact shape of the original #2859 CI regression.
20
+ */
21
+ import { describe, test, expect } from 'bun:test'
22
+ import { compileJSX } from '../compiler'
23
+ import { TestAdapter } from '../adapters/test-adapter'
24
+
25
+ const adapter = new TestAdapter()
26
+
27
+ function clientJsFor(source: string): string {
28
+ const result = compileJSX(source, 'Repro.tsx', { adapter })
29
+ const errors = result.errors.filter(e => e.severity === 'error')
30
+ if (errors.length > 0) throw new Error(errors.map(e => e.message).join('\n'))
31
+ return result.files.find(f => f.type === 'clientJs')!.content
32
+ }
33
+
34
+ describe('buildPlainRowCore — shared wrap-item/decide-lazy/wrap-index sequence', () => {
35
+ test('top-level plain loop: a ref forces eager, and the index is wrapped into a live accessor call', () => {
36
+ const js = clientJsFor(`
37
+ 'use client'
38
+ import { createSignal } from '@barefootjs/client'
39
+ type Row = { id: number; label: string }
40
+ export function Repro() {
41
+ const [rows] = createSignal<Row[]>([])
42
+ const refs: HTMLLIElement[] = []
43
+ return <ul>{rows().map((row, i) => <li key={row.id} ref={el => { refs[i] = el! }}>{String(i + 1)}. {row.label}</li>)}</ul>
44
+ }
45
+ `)
46
+ expect(js).not.toContain('mapArrayLazy(')
47
+ expect(js).toMatch(/i\(\)\s*\+\s*1/)
48
+ })
49
+
50
+ test('branch-plain loop (ternary): the SAME ref-bearing shape wraps the index identically', () => {
51
+ const js = clientJsFor(`
52
+ 'use client'
53
+ import { createSignal } from '@barefootjs/client'
54
+ type Row = { id: number; label: string }
55
+ export function Repro() {
56
+ const [active, setActive] = createSignal(true)
57
+ const [rows] = createSignal<Row[]>([])
58
+ const refs: HTMLLIElement[] = []
59
+ return <div>{active() ? rows().map((row, i) => <li key={row.id} ref={el => { refs[i] = el! }}>{String(i + 1)}. {row.label}</li>) : <span>Empty</span>}</div>
60
+ }
61
+ `)
62
+ expect(js).not.toContain('mapArrayLazy(')
63
+ expect(js).toMatch(/i\(\)\s*\+\s*1/)
64
+ })
65
+
66
+ test('top-level plain loop: a lazy-eligible row (no index reference) goes lazy with no leaked index wrap', () => {
67
+ const js = clientJsFor(`
68
+ 'use client'
69
+ import { createSignal } from '@barefootjs/client'
70
+ type Row = { id: number; label: string }
71
+ export function Repro() {
72
+ const [rows] = createSignal<Row[]>([{ id: 1, label: 'alpha' }])
73
+ const [selected, setSelected] = createSignal(1)
74
+ return (
75
+ <ul>
76
+ {rows().map(row => (
77
+ <li key={row.id} onClick={() => setSelected(row.id)}>{String(selected() === row.id ? 'YES' : 'NO')}</li>
78
+ ))}
79
+ </ul>
80
+ )
81
+ }
82
+ `)
83
+ expect(js).toContain('mapArrayLazy(')
84
+ })
85
+
86
+ test('branch-plain loop (ternary): the SAME lazy-eligible shape also goes lazy', () => {
87
+ const js = clientJsFor(`
88
+ 'use client'
89
+ import { createSignal } from '@barefootjs/client'
90
+ type Row = { id: number; label: string }
91
+ export function Repro() {
92
+ const [active] = createSignal(true)
93
+ const [rows] = createSignal<Row[]>([{ id: 1, label: 'alpha' }])
94
+ const [selected, setSelected] = createSignal(1)
95
+ return (
96
+ <div>
97
+ {active() ? rows().map(row => (
98
+ <li key={row.id} onClick={() => setSelected(row.id)}>{String(selected() === row.id ? 'YES' : 'NO')}</li>
99
+ )) : <span>Empty</span>}
100
+ </div>
101
+ )
102
+ }
103
+ `)
104
+ expect(js).toContain('mapArrayLazy(')
105
+ })
106
+
107
+ test('a ref callback closing over the loop index reads the live accessor at the top-level call site (#2859)', () => {
108
+ const js = clientJsFor(`
109
+ 'use client'
110
+ import { createSignal } from '@barefootjs/client'
111
+ type Row = { id: number; label: string }
112
+ export function Repro() {
113
+ const [rows] = createSignal<Row[]>([])
114
+ const refs: HTMLLIElement[] = []
115
+ return <ul>{rows().map((row, i) => <li key={row.id} ref={el => { refs[i] = el! }}>{row.label}</li>)}</ul>
116
+ }
117
+ `)
118
+ expect(js).toMatch(/refs\[i\(\)\]/)
119
+ })
120
+ })
@@ -260,7 +260,9 @@ describe('Solid-style wrap-by-default fallback for child-component props (#942)'
260
260
  // The emitted code rewrites `props.xxx` to the internal props-param
261
261
  // name (`_p.xxx`) via rewriteDestructuredPropsInExpr. Assert on the
262
262
  // emitted form — the gate's `expandedValue.includes('props.')` check
263
- // still runs against the source expression before rewriting.
264
- expect(clientJs).toMatch(/__v\s*=\s*_p\.title/)
263
+ // still runs against the source expression before rewriting. The
264
+ // dedup-guarded write (#2869) computes into `__x` first, then assigns
265
+ // `__v = __x` inside `emitAttrUpdate`'s own generic branch.
266
+ expect(clientJs).toMatch(/__x\s*=\s*_p\.title/)
265
267
  })
266
268
  })