@barefootjs/jsx 0.26.4 → 0.28.0

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 (106) hide show
  1. package/dist/compiler.d.ts.map +1 -1
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +941 -130
  5. package/dist/ir-to-client-js/client-only-elision.d.ts +103 -0
  6. package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -0
  7. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +6 -0
  9. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts +2 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts +7 -0
  13. package/dist/ir-to-client-js/control-flow/plan/build-insert.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +107 -0
  15. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -0
  16. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts +9 -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/lazy-row-eligibility.d.ts +203 -0
  19. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -0
  20. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +10 -0
  21. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/shared.d.ts +1 -1
  23. package/dist/ir-to-client-js/control-flow/stringify/branch-loop.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts +54 -0
  25. package/dist/ir-to-client-js/control-flow/stringify/claim-plan.d.ts.map +1 -0
  26. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/stringify/insert.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +98 -0
  29. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -0
  30. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +5 -0
  31. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +17 -20
  33. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  34. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +63 -4
  35. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  36. package/dist/ir-to-client-js/control-flow.d.ts +1 -1
  37. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  38. package/dist/ir-to-client-js/element-refs.d.ts +8 -3
  39. package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
  40. package/dist/ir-to-client-js/emit-reactive.d.ts +8 -1
  41. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  42. package/dist/ir-to-client-js/html-template.d.ts +24 -0
  43. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  44. package/dist/ir-to-client-js/imports.d.ts +2 -2
  45. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  46. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  47. package/dist/ir-to-client-js/types.d.ts +9 -1
  48. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  49. package/dist/ir-to-client-js/utils.d.ts +1 -1
  50. package/dist/types.d.ts +25 -2
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +2 -2
  53. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +542 -689
  54. package/src/__tests__/child-components-in-map.test.ts +4 -4
  55. package/src/__tests__/client-js-generation.test.ts +23 -12
  56. package/src/__tests__/composite-branch-loop.test.ts +14 -5
  57. package/src/__tests__/conditional-mapArray-key.test.ts +7 -1
  58. package/src/__tests__/create-selector.test.ts +21 -7
  59. package/src/__tests__/cross-file-client-signal.test.ts +5 -1
  60. package/src/__tests__/early-return-scope-var-ref.test.ts +10 -11
  61. package/src/__tests__/inline-jsx-callback.test.ts +6 -4
  62. package/src/__tests__/ir-jsx-props.test.ts +6 -5
  63. package/src/__tests__/lazy-row-eligibility.test.ts +692 -0
  64. package/src/__tests__/loop-branch-bare-expression-reactive-text.test.ts +98 -0
  65. package/src/__tests__/loop-fallback-wrap.test.ts +34 -16
  66. package/src/__tests__/loop-hoisted-template.test.ts +10 -6
  67. package/src/__tests__/module-level-signal.audit.test.ts +7 -2
  68. package/src/__tests__/nested-loop-conditional.test.ts +9 -7
  69. package/src/__tests__/nested-loop-reactive-attrs.test.ts +2 -2
  70. package/src/__tests__/preamble-region-patch.test.ts +18 -12
  71. package/src/__tests__/profile-cond-binding-ids.test.ts +3 -2
  72. package/src/__tests__/profile-loop-binding-ids.test.ts +4 -3
  73. package/src/__tests__/profile-nested-binding-ids.test.ts +11 -6
  74. package/src/__tests__/reactive-attrs-in-map.test.ts +6 -4
  75. package/src/__tests__/static-loop-csr-materialize.test.ts +6 -1
  76. package/src/__tests__/template-closure.test.ts +1 -1
  77. package/src/compiler.ts +11 -0
  78. package/src/index.ts +1 -0
  79. package/src/ir-to-client-js/client-only-elision.ts +273 -0
  80. package/src/ir-to-client-js/collect-elements.ts +56 -17
  81. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +6 -0
  82. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +35 -12
  83. package/src/ir-to-client-js/control-flow/plan/build-insert.ts +9 -2
  84. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +305 -0
  85. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +44 -12
  86. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +440 -0
  87. package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -0
  88. package/src/ir-to-client-js/control-flow/shared.ts +1 -1
  89. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +34 -4
  90. package/src/ir-to-client-js/control-flow/stringify/claim-plan.ts +68 -0
  91. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +18 -9
  92. package/src/ir-to-client-js/control-flow/stringify/insert.ts +17 -11
  93. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +478 -0
  94. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +41 -17
  95. package/src/ir-to-client-js/control-flow/stringify/loop.ts +71 -46
  96. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +241 -31
  97. package/src/ir-to-client-js/control-flow.ts +8 -3
  98. package/src/ir-to-client-js/element-refs.ts +9 -11
  99. package/src/ir-to-client-js/emit-reactive.ts +50 -14
  100. package/src/ir-to-client-js/html-template.ts +29 -9
  101. package/src/ir-to-client-js/imports.ts +13 -4
  102. package/src/ir-to-client-js/reactivity.ts +3 -2
  103. package/src/ir-to-client-js/types.ts +10 -2
  104. package/src/ir-to-client-js/utils.ts +1 -1
  105. package/src/jsx-to-ir.ts +26 -8
  106. package/src/types.ts +25 -2
@@ -211,7 +211,7 @@ describe('child components inside .map() (#344)', () => {
211
211
  expect(clientJs!.content).toContain('toggleRow(index)')
212
212
  })
213
213
 
214
- test('dynamic signal array: component generates reconcileElements with createComponent', () => {
214
+ test('dynamic signal array: component generates mapArray with createComponent', () => {
215
215
  const source = `
216
216
  'use client'
217
217
  import { createSignal } from '@barefootjs/client'
@@ -321,7 +321,7 @@ describe('child components inside .map() (#344)', () => {
321
321
  const uniqueVars = new Set(allVars)
322
322
  expect(allVars.length).toBe(uniqueVars.size)
323
323
 
324
- // Component slot ref ($c) and reconcileElements should both be present
324
+ // Component slot ref ($c) and mapArray should both be present
325
325
  expect(content).toContain('$c(__scope')
326
326
  expect(content).toContain('mapArray')
327
327
  })
@@ -355,7 +355,7 @@ describe('child components inside .map() (#344)', () => {
355
355
  expect(clientJs).toBeDefined()
356
356
  const content = clientJs!.content
357
357
 
358
- // Should use reconcileElements with createComponent
358
+ // Should use mapArray with createComponent
359
359
  expect(content).toContain('mapArray')
360
360
  expect(content).toContain("createComponent('TableRow'")
361
361
 
@@ -650,7 +650,7 @@ describe('child components inside .map() (#344)', () => {
650
650
  expect(clientJs).toBeDefined()
651
651
  const content = clientJs!.content
652
652
 
653
- // Dynamic array should use reconcileElements and event delegation
653
+ // Dynamic array should use mapArray and event delegation
654
654
  expect(content).toContain('mapArray')
655
655
  expect(content).toContain(".addEventListener('click', (__bfEvt) => {")
656
656
  expect(content).toContain('handleClick(item.id)')
@@ -151,7 +151,7 @@ describe('Client JS generation', () => {
151
151
  })
152
152
 
153
153
  describe('map with index parameter', () => {
154
- test('includes index parameter in reconcileElements renderItem callback', () => {
154
+ test('includes index parameter in mapArray renderItem callback', () => {
155
155
  const source = `
156
156
  'use client'
157
157
  import { createMemo } from '@barefootjs/client'
@@ -1760,7 +1760,10 @@ describe('Client JS generation', () => {
1760
1760
  // update. See `preamble-region-patch.test.ts` for the full region
1761
1761
  // contract (row-template marker, region-patch effect, SSR parity).
1762
1762
  expect(clientJs?.content).toContain('${escapeText(label)}')
1763
- expect(clientJs?.content).toContain('patchSlotRange')
1763
+ // Slot unification A3: preamble regions now write through a claimed
1764
+ // 'markup' slot instead of `patchSlotRange`.
1765
+ expect(clientJs?.content).toContain("kind: 'markup'")
1766
+ expect(clientJs?.content).toContain('lazySlots(__el')
1764
1767
  })
1765
1768
 
1766
1769
  test('handles block body with multiple variable declarations', () => {
@@ -2423,11 +2426,12 @@ describe('Client JS generation', () => {
2423
2426
  expect(clientJs).toBeDefined()
2424
2427
  const content = clientJs!.content
2425
2428
 
2426
- // Text node updates route through `__bfText`, which renders nullish
2427
- // values as '' (not the string "undefined") and also splices live
2428
- // Nodes by identity (#1663). The nullish guard moved from the inline
2429
- // `String(__val ?? '')` assignment into that helper.
2430
- expect(content).toContain('__bfText(')
2429
+ // Text node updates route through a claimed 'markup' slot writer (slot
2430
+ // unification A3), whose `writeMarkup` renders nullish values as ''
2431
+ // (not the string "undefined") and also splices live Nodes by
2432
+ // identity (#1663) the same contract `__bfText` used to provide.
2433
+ // The nullish guard lives in the runtime writer, not inline here.
2434
+ expect(content).toContain("kind: 'markup'")
2431
2435
  expect(content).not.toMatch(/\.nodeValue = String\(__val\)(?! )/)
2432
2436
  })
2433
2437
 
@@ -2464,8 +2468,10 @@ describe('Client JS generation', () => {
2464
2468
  expect(clientJs).toBeDefined()
2465
2469
  const content = clientJs!.content
2466
2470
 
2467
- // text() inside a component inside a conditional uses $t() in branch-scoped effect
2468
- expect(content).toContain('$t(__branchScope')
2471
+ // text() inside a component inside a conditional writes through a
2472
+ // claimed 'markup' slot built fresh in the branch's own bindEvents
2473
+ // (slot unification A3) — re-claimed on every branch activation.
2474
+ expect(content).toContain("lazySlots(__branchScope, [{ id: '^s2', kind: 'markup', path: [] }])")
2469
2475
  })
2470
2476
 
2471
2477
  test('propagates insideConditional through fragment children', () => {
@@ -2495,8 +2501,10 @@ describe('Client JS generation', () => {
2495
2501
  expect(clientJs).toBeDefined()
2496
2502
  const content = clientJs!.content
2497
2503
 
2498
- // text() inside a fragment inside a conditional uses $t() in branch-scoped effect
2499
- expect(content).toContain('$t(__branchScope')
2504
+ // text() inside a fragment inside a conditional writes through a
2505
+ // claimed 'markup' slot built fresh in the branch's own bindEvents
2506
+ // (slot unification A3) — re-claimed on every branch activation.
2507
+ expect(content).toContain("lazySlots(__branchScope, [{ id: 's2', kind: 'markup', path: [] }])")
2500
2508
  })
2501
2509
  })
2502
2510
 
@@ -2630,7 +2638,10 @@ describe('Client JS generation', () => {
2630
2638
  const js = result.files.find(f => f.type === 'clientJs')!.content
2631
2639
  // The branch arm's mapArray call must live inside a
2632
2640
  // createDisposableEffect that is pushed into __disposers.
2633
- expect(js).toMatch(/__disposers\.push\(createDisposableEffect\(\(\)\s*=>\s*\{[\s\S]*?mapArray\(/)
2641
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
2642
+ // (spec/slot-unification.md §9.4) uses the latter and has exactly the
2643
+ // same disposal obligation.
2644
+ expect(js).toMatch(/__disposers\.push\(createDisposableEffect\(\(\)\s*=>\s*\{[\s\S]*?mapArray(Lazy)?\(/)
2634
2645
  })
2635
2646
 
2636
2647
  test('nested conditional inside a conditional branch is wrapped in createDisposableEffect', () => {
@@ -2,7 +2,7 @@
2
2
  * BarefootJS Compiler - Composite loops inside conditional branches (#724)
3
3
  *
4
4
  * Verifies that loops with child components inside ternary conditionals
5
- * generate reconcileElements with createComponent in the branch's bindEvents.
5
+ * generate mapArray with createComponent in the branch's bindEvents.
6
6
  */
7
7
 
8
8
  import { describe, test, expect } from 'bun:test'
@@ -52,8 +52,11 @@ describe('composite loops inside conditional branches (#724)', () => {
52
52
  expect(clientJs).toBeDefined()
53
53
  const js = clientJs!.content
54
54
 
55
- // Should use reconcileElements inside the branch's bindEvents
56
- expect(js).toContain('mapArray(')
55
+ // Should use mapArray inside the branch's bindEvents
56
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
57
+ // (spec/slot-unification.md §9.4) uses the latter; either satisfies
58
+ // "keyed reconciliation, not a composite/component shape".
59
+ expect(js).toMatch(/\bmapArray(Lazy)?\(/)
57
60
 
58
61
  // After O-1's mode-collapse PR, child component init goes through the
59
62
  // runtime `upsertChild` helper instead of an emit-side
@@ -96,7 +99,10 @@ describe('composite loops inside conditional branches (#724)', () => {
96
99
  const js = clientJs!.content
97
100
 
98
101
  // Should use mapArray for per-item reactivity
99
- expect(js).toContain('mapArray(')
102
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
103
+ // (spec/slot-unification.md §9.4) uses the latter; either satisfies
104
+ // "keyed reconciliation, not a composite/component shape".
105
+ expect(js).toMatch(/\bmapArray(Lazy)?\(/)
100
106
 
101
107
  // Should NOT use createComponent inside the init body (no child
102
108
  // components in this fixture). The CLI also emits a callable shim
@@ -147,7 +153,10 @@ describe('composite loops inside conditional branches (#724)', () => {
147
153
  const js = clientJs!.content
148
154
 
149
155
  // Should use mapArray for the loop
150
- expect(js).toContain('mapArray(')
156
+ // `mapArray(` or `mapArrayLazy(` — a lazy-row-eligible branch loop
157
+ // (spec/slot-unification.md §9.4) uses the latter; either satisfies
158
+ // "keyed reconciliation, not a composite/component shape".
159
+ expect(js).toMatch(/\bmapArray(Lazy)?\(/)
151
160
 
152
161
  // Should have event delegation (addEventListener + closest pattern)
153
162
  expect(js).toContain(".addEventListener('click', (__bfEvt) => {")
@@ -29,11 +29,17 @@ function clientJs(source: string, file = 'CondKey.tsx'): string {
29
29
  return cjs!.content
30
30
  }
31
31
 
32
+ /**
33
+ * Reconciler call sites, whichever family they use. A lazy-row-eligible loop
34
+ * (spec/slot-unification.md §9.4) emits `mapArrayLazy(`; everything else keeps
35
+ * `mapArray(`. The key expression this suite asserts on is the same third
36
+ * argument in both shapes, so match the family, not one member of it.
37
+ */
32
38
  function mapArrayCalls(content: string): string[] {
33
39
  return content
34
40
  .split('\n')
35
41
  .map((ln) => ln.trim())
36
- .filter((ln) => ln.startsWith('mapArray('))
42
+ .filter((ln) => ln.startsWith('mapArray(') || ln.startsWith('mapArrayLazy('))
37
43
  }
38
44
 
39
45
  describe('mapArray key extraction across conditional branches (#1098)', () => {
@@ -7,9 +7,14 @@
7
7
  * `isSelected(row.id)` as reactive with no analyzer changes — the callee's
8
8
  * type carries the brand, not the call's return type. These tests pin:
9
9
  * - the classic js-framework-benchmark row compiles with `isSelected(row.id)`
10
- * wired to a single per-row `createEffect` (not the legacy
11
- * `selected() === row.id` shape, which would still work but subscribes
12
- * every row to the raw signal)
10
+ * wired through the lazy row graph — ZERO emitted per-row effects, the
11
+ * loop-level effect living in `mapArrayLazy` (spec §9). Until §9.3a
12
+ * was lifted this shape was gate-INELIGIBLE (an opaque local whose call
13
+ * is the reactive read could not be primed) and compiled to one per-row
14
+ * `createEffect`; the runtime re-subscribe seam replaced that, so the
15
+ * loop is emitted like any other. Either way it is never
16
+ * the legacy `selected() === row.id` shape, which subscribes every row
17
+ * to the raw signal.
13
18
  * - the loop-param accessor wrap applies only to the call ARGUMENT
14
19
  * (`row.id` -> `row().id`), leaving the `isSelected` callee untouched
15
20
  * - the hoisted skeleton fast path (#2223) still fires for a selector-driven
@@ -36,7 +41,7 @@ function compile(source: string, filename: string): { clientJs: string; template
36
41
  }
37
42
 
38
43
  describe('createSelector (perf, #2143 gap 5)', () => {
39
- test('isSelected(row.id) in a loop body compiles to one per-row createEffect', () => {
44
+ test('isSelected(row.id) in a loop body compiles through the lazy row graph', () => {
40
45
  const source = `
41
46
  'use client'
42
47
  import { createSignal, createSelector } from '@barefootjs/client'
@@ -66,12 +71,21 @@ describe('createSelector (perf, #2143 gap 5)', () => {
66
71
  // The callee itself is never rewritten to an accessor call.
67
72
  expect(clientJs).not.toContain('isSelected()(')
68
73
 
69
- // Exactly one createEffect wraps the selector-driven class attribute
70
- // not the O(rows) `selected() === row.id` shape.
74
+ // The selector-driven class attribute costs NO emitted effect at all:
75
+ // the loop is lazy-eligible, so its one loop-level effect lives inside
76
+ // `mapArrayLazy` rather than in the emitted row body. It is also never
77
+ // the O(rows) `selected() === row.id` shape.
71
78
  const effectCount = (clientJs.match(/createEffect\(/g) ?? []).length
72
- expect(effectCount).toBe(1)
79
+ expect(effectCount).toBe(0)
80
+ expect(clientJs).toContain('mapArrayLazy(')
73
81
  expect(clientJs).not.toContain('selected() ===')
74
82
 
83
+ // `isSelected` is an opaque local (its CALL is the reactive read). Nothing
84
+ // in the emission marks that: the runtime's re-subscribe seam applies to
85
+ // every loop-level outer effect, so the compiler has no judgement to
86
+ // encode here (§9.3a).
87
+ expect(clientJs).not.toContain('outerNeedsResubscribe')
88
+
75
89
  // The hoisted skeleton fast path (#2223) still fires: a single-line
76
90
  // renderItem clones from a once-per-loop template.
77
91
  expect(clientJs).toMatch(/const __tpl_\w+ = document\.createElement\('template'\)/)
@@ -104,7 +104,11 @@ export function Viewer() {
104
104
  const files = Object.fromEntries(r.files.map(f => [f.path, f.content]))
105
105
  const ssr = Object.values(files).find(c => c.includes('export function Viewer'))
106
106
  expect(ssr).toBeDefined()
107
- expect(ssr).toContain('client:s')
107
+ // Slot unification Step B: `{val()}` is the SPAN's only, non-adjacent
108
+ // child, so its marker pair is elided entirely (see the identical note
109
+ // in module-level-signal.audit.test.ts) — the client's claim plan
110
+ // creates the missing Text node from `elidedPath` alone.
111
+ expect(ssr).not.toContain('bfText(')
108
112
  expect(ssr).not.toContain("const val = () =>")
109
113
  })
110
114
 
@@ -65,12 +65,10 @@ describe('JSX expression referencing a local declared inside an early-return if-
65
65
 
66
66
  const content = clientJsContent(result)
67
67
 
68
- // Pre-fix: emitted `updateClientMarker(__scope, 's4', aLocal)` at
69
- // outer init scope `aLocal` undefined. Post-fix: the JSX is
70
- // inlined directly into the template, so the createEffect /
71
- // updateClientMarker block is not needed at all for this static
72
- // shape.
73
- expect(content).not.toContain('updateClientMarker')
68
+ // Pre-fix, `aLocal` leaked into a reactive-update block at outer init
69
+ // scope where it doesn't exist (see file docstring). Post-fix: the JSX
70
+ // is inlined directly into the template, so no reactive-update block is
71
+ // needed at all for this static shape.
74
72
  expect(content).not.toMatch(/\baLocal\b/)
75
73
 
76
74
  const hydrate = hydrateLine(result)
@@ -97,9 +95,10 @@ describe('JSX expression referencing a local declared inside an early-return if-
97
95
  const content = clientJsContent(result)
98
96
  // The bare `label` identifier must not appear in the emitted init
99
97
  // function — it's resolved to its initializer value at IR-build
100
- // time.
101
- expect(content).not.toMatch(/updateClientMarker\([^)]*\blabel\b[^)]*\)/)
102
- expect(content).toContain("updateClientMarker(__scope, 's4', 'hello')")
98
+ // time. `@client` writes through a claimed 'text' slot writer
99
+ // (`claim-slots.ts`).
100
+ expect(content).not.toMatch(/__bfw_\w+\([^)]*\blabel\b[^)]*\)/)
101
+ expect(content).toContain("__bfw_s4('s4', 'hello')")
103
102
  })
104
103
 
105
104
  test('ternary scope var (`readOnly ? null : <jsx/>`) routes to client-only conditional', () => {
@@ -124,8 +123,8 @@ describe('JSX expression referencing a local declared inside an early-return if-
124
123
  expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
125
124
 
126
125
  const content = clientJsContent(result)
127
- // Pre-fix: `updateClientMarker(__scope, 's*', compactResize)`
128
- // referenced an undeclared name. Post-fix: the ternary becomes an
126
+ // Pre-fix, `compactResize` leaked into the emitted reactive-update
127
+ // block as an undeclared name. Post-fix: the ternary becomes an
129
128
  // IRConditional with `clientOnly: true`, routed through `insert()`
130
129
  // — and `props.readOnly` is rewritten to `_p.readOnly`.
131
130
  expect(content).not.toMatch(/\bcompactResize\b/)
@@ -440,10 +440,12 @@ describe('inline JSX as object-literal arrow value (#1663)', () => {
440
440
  expect(js).toMatch(/hydrate\(\s*['"]BFInlineJsxCallback1(?:__|['"])/)
441
441
  })
442
442
 
443
- test('dynamic JSX-returning call routes the slot through __bfText', () => {
443
+ test('dynamic JSX-returning call routes the slot through a claimed markup slot', () => {
444
444
  // The child expression `{themeLogo(props.id)}` evaluates to a live
445
445
  // component element on the client; the slot update must splice it in by
446
- // identity via __bfText rather than stringify it into "[object …]".
446
+ // identity via a claimed 'markup' slot writer (slot unification A3)
447
+ // rather than stringify it into "[object …]" — the same contract
448
+ // `__bfText` used to provide.
447
449
  const source = `
448
450
  'use client'
449
451
  import { BrandLogo } from './brand-logo'
@@ -458,7 +460,7 @@ describe('inline JSX as object-literal arrow value (#1663)', () => {
458
460
  }
459
461
  `
460
462
  const js = clientJs(source)
461
- expect(js).toContain('__bfText(')
462
- expect(js).toMatch(/__bfText[^']*from '@barefootjs\/client\/runtime'/)
463
+ expect(js).toContain("kind: 'markup'")
464
+ expect(js).toMatch(/lazySlots[^']*from '@barefootjs\/client\/runtime'/)
463
465
  })
464
466
  })
@@ -359,11 +359,12 @@ describe('JSX props (#559)', () => {
359
359
 
360
360
  const clientJs = result.files.find(f => f.type === 'clientJs')
361
361
  expect(clientJs).toBeDefined()
362
- // The text update routes through `__bfText`, which preserves the
363
- // server-rendered DOM for `__isSlot` values (and splices live Nodes
364
- // by identity) instead of the old inline `nodeValue = String(...)`
365
- // assignment. The `__isSlot` guard now lives inside that helper (#1663).
366
- expect(clientJs!.content).toContain('__bfText')
362
+ // The text update routes through a claimed 'markup' slot writer (slot
363
+ // unification A3), whose `writeMarkup` preserves the server-rendered
364
+ // DOM for `__isSlot` values (and splices live Nodes by identity)
365
+ // instead of the old inline `nodeValue = String(...)` assignment
366
+ // the same contract `__bfText` used to provide (#1663).
367
+ expect(clientJs!.content).toContain("kind: 'markup'")
367
368
  })
368
369
  })
369
370
  })