@barefootjs/jsx 0.26.3 → 0.26.4

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 (54) hide show
  1. package/dist/index.js +433 -55
  2. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  3. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  4. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +12 -1
  5. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  6. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  7. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
  10. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +27 -0
  12. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/shared.d.ts +12 -1
  14. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -0
  17. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/html-template.d.ts +49 -1
  19. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/imports.d.ts +2 -2
  21. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/types.d.ts +26 -1
  24. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  25. package/dist/jsx-to-ir.d.ts.map +1 -1
  26. package/dist/types.d.ts +47 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +187 -37
  30. package/src/__tests__/client-js-generation.test.ts +8 -2
  31. package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
  32. package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
  33. package/src/__tests__/flatmap-segments.test.ts +182 -0
  34. package/src/__tests__/map-body-no-silent-divergence.test.ts +45 -0
  35. package/src/__tests__/preamble-region-patch.test.ts +150 -0
  36. package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
  37. package/src/ir-to-client-js/build-references.ts +16 -0
  38. package/src/ir-to-client-js/collect-elements.ts +66 -10
  39. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +12 -1
  40. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +11 -3
  41. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
  42. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +36 -0
  43. package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
  44. package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
  45. package/src/ir-to-client-js/control-flow/shared.ts +26 -1
  46. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +25 -3
  47. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +67 -19
  48. package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
  49. package/src/ir-to-client-js/html-template.ts +115 -6
  50. package/src/ir-to-client-js/imports.ts +1 -1
  51. package/src/ir-to-client-js/reactivity.ts +6 -0
  52. package/src/ir-to-client-js/types.ts +24 -0
  53. package/src/jsx-to-ir.ts +374 -8
  54. package/src/types.ts +49 -0
@@ -6,7 +6,7 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
6
6
  import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
7
7
  import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
8
8
  import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
9
- import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, type SkeletonSlotPaths } from './html-template.ts'
9
+ import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
10
10
  import { templateRootIsSvg } from './control-flow/stringify/template-parse.ts'
11
11
  import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
12
12
  import { walkIR, stopAt } from './walker.ts'
@@ -650,10 +650,26 @@ export function collectElements(
650
650
  // are extracted explicitly below for the closure capture set.
651
651
  if (!l.slotId || inCond) return
652
652
 
653
+ // flatMap PROJECTION loop: the only child is a nested IRLoop lowered
654
+ // from `flatMap(it => it.tags.map(...))`. The children exist for SSR
655
+ // templatization (nested {{range}} on DSL adapters); the CLIENT
656
+ // reconciles the flattened leaves through the descriptor mapArray
657
+ // path synthesized from the same inner loop, so none of the
658
+ // children-derived client machinery (bindings, inner-loop plans,
659
+ // per-row template) applies — leaf wiring was refused at IR build.
660
+ const projectionInner =
661
+ l.method === 'flatMap' && l.children.length === 1 && l.children[0].type === 'loop'
662
+ ? l.children[0]
663
+ : undefined
664
+
653
665
  const childHandlers: string[] = []
654
- const bindings = collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings)
655
- for (const child of l.children) {
656
- childHandlers.push(...collectEventHandlersFromIR(child))
666
+ const bindings = projectionInner
667
+ ? emptyLoopChildBindings()
668
+ : collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings)
669
+ if (!projectionInner) {
670
+ for (const child of l.children) {
671
+ childHandlers.push(...collectEventHandlersFromIR(child))
672
+ }
657
673
  }
658
674
 
659
675
  if (l.childComponent) {
@@ -668,7 +684,9 @@ export function collectElements(
668
684
  // Determine rendering strategy for dynamic arrays:
669
685
  // Use element reconciliation when the loop body has nested components,
670
686
  // or when inner loops need their own mapArray for events/reactive text.
671
- const { useElementReconciliation, innerLoops } = decideLoopRendering(l, siblingOffsets, ctx)
687
+ const { useElementReconciliation, innerLoops } = projectionInner
688
+ ? { useElementReconciliation: false, innerLoops: undefined }
689
+ : decideLoopRendering(l, siblingOffsets, ctx)
672
690
 
673
691
  let template = ''
674
692
  let staticItemTemplate: string | undefined
@@ -691,7 +709,7 @@ export function collectElements(
691
709
  // `data-key`, mirroring the SSR template's renderChild emit.
692
710
  staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, undefined, undefined, /* insideLoop */ true)
693
711
  }
694
- } else if (l.children[0]) {
712
+ } else if (l.children[0] && !projectionInner) {
695
713
  // Pass loopParams so expressions are wrapped at generation time,
696
714
  // avoiding post-hoc regex wrapping that corrupts literal attribute values.
697
715
  // Forward destructured bindings (#951) so references like `cfg.color`
@@ -777,6 +795,20 @@ export function collectElements(
777
795
  } : undefined,
778
796
  chainOrder: l.chainOrder,
779
797
  preamble: l.preamble,
798
+ preambleRegions: l.preambleRegions,
799
+ flatMapClient: projectionInner
800
+ ? {
801
+ params: l.index ? `(${l.param}, ${l.index})` : `(${l.param})`,
802
+ body: renderFlatMapProjectionClientBody(projectionInner, buildRestSpreadNames(ctx)),
803
+ keyed: projectionInner.key !== null,
804
+ }
805
+ : l.flatMapCallback
806
+ ? {
807
+ params: l.flatMapCallback.params,
808
+ body: renderFlatMapClientBody(l.flatMapCallback, buildRestSpreadNames(ctx)),
809
+ keyed: flatMapCallbackHasKeyedLeaf(l.flatMapCallback),
810
+ }
811
+ : undefined,
780
812
  })
781
813
  // Don't descend — loop-scoped variables are only available inside the iteration.
782
814
  },
@@ -1069,8 +1101,16 @@ function collectBranchLoops(
1069
1101
  // reactive-text collection on inner loops reached from this call path
1070
1102
  // is handled at the enclosing branch-loop level below (`if (ctx)` block
1071
1103
  // around `collectLoopChildReactiveTexts`), not per inner loop.
1072
- const { useElementReconciliation, innerLoops: innerLoopsCollected } =
1073
- decideLoopRendering(n, siblingOffsets, undefined)
1104
+ // flatMap PROJECTION loop see the top-level handler's note: children
1105
+ // are SSR-only; the client rides the descriptor mapArray path.
1106
+ const projectionInner =
1107
+ n.method === 'flatMap' && n.children.length === 1 && n.children[0].type === 'loop'
1108
+ ? n.children[0]
1109
+ : undefined
1110
+
1111
+ const { useElementReconciliation, innerLoops: innerLoopsCollected } = projectionInner
1112
+ ? { useElementReconciliation: false, innerLoops: undefined }
1113
+ : decideLoopRendering(n, siblingOffsets, undefined)
1074
1114
 
1075
1115
  // Build the item template from loop children.
1076
1116
  // Use loopDepth=0: this loop gets its own reconcileElements (independent
@@ -1080,7 +1120,9 @@ function collectBranchLoops(
1080
1120
  // use `param()` to read the current item value.
1081
1121
  let childTemplate: string
1082
1122
  const branchLoopParamSpec = [{ param: n.param, bindings: n.paramBindings }]
1083
- if (useElementReconciliation && n.children[0]) {
1123
+ if (projectionInner) {
1124
+ childTemplate = '' // descriptor renderItem builds from d.h, not a row template
1125
+ } else if (useElementReconciliation && n.children[0]) {
1084
1126
  childTemplate = irToPlaceholderTemplate(n.children[0], restNames, 0, branchLoopParamSpec)
1085
1127
  } else {
1086
1128
  childTemplate = n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpec)).join('')
@@ -1092,7 +1134,7 @@ function collectBranchLoops(
1092
1134
  // memos). Previously these were only collected for composite loops,
1093
1135
  // which caused reactive reads inside simple loop bodies to silently
1094
1136
  // no-op for existing items.
1095
- const branchBindings = ctx
1137
+ const branchBindings = ctx && !projectionInner
1096
1138
  ? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings)
1097
1139
  : emptyLoopChildBindings()
1098
1140
 
@@ -1112,6 +1154,7 @@ function collectBranchLoops(
1112
1154
  template: childTemplate,
1113
1155
  containerSlotId: containerSlot,
1114
1156
  preamble: n.preamble,
1157
+ preambleRegions: n.preambleRegions,
1115
1158
  nestedComponents: useElementReconciliation ? n.nestedComponents : undefined,
1116
1159
  bindings: branchBindings,
1117
1160
  innerLoops: useElementReconciliation ? innerLoopsCollected : undefined,
@@ -1126,6 +1169,19 @@ function collectBranchLoops(
1126
1169
  raw: n.sortComparator.raw,
1127
1170
  } : undefined,
1128
1171
  chainOrder: n.chainOrder,
1172
+ flatMapClient: projectionInner
1173
+ ? {
1174
+ params: n.index ? `(${n.param}, ${n.index})` : `(${n.param})`,
1175
+ body: renderFlatMapProjectionClientBody(projectionInner, restNames),
1176
+ keyed: projectionInner.key !== null,
1177
+ }
1178
+ : n.flatMapCallback
1179
+ ? {
1180
+ params: n.flatMapCallback.params,
1181
+ body: renderFlatMapClientBody(n.flatMapCallback, restNames),
1182
+ keyed: flatMapCallbackHasKeyedLeaf(n.flatMapCallback),
1183
+ }
1184
+ : undefined,
1129
1185
  })
1130
1186
  // Don't recurse into the loop — nested loops are handled by the loop's own reconciliation.
1131
1187
  },
@@ -15,11 +15,17 @@
15
15
  * pair.
16
16
  */
17
17
 
18
- import type { CompositeLoopPlan, LoopChildRefBinding } from './loop.ts'
18
+ import type { CompositeLoopPlan, LoopChildRefBinding, PreambleRegionPlan } from './loop.ts'
19
19
  import type { EventDelegationPlan } from './event-delegation.ts'
20
20
  import type { ReactiveEffectsPlan } from './reactive-effects.ts'
21
21
 
22
22
  export interface BranchPlainLoopPlan {
23
+ /**
24
+ * flatMap descriptor mode — see `PlainLoopVariant.flatMapLeafItem`
25
+ * (plan/loop.ts): `arrayExpr` already flattens through the descriptor
26
+ * body and the stringifier emits the build-or-patch renderItem.
27
+ */
28
+ flatMapLeafItem?: boolean
23
29
  kind: 'plain'
24
30
  /** See LoopPlanCommon.rowConstruction — required on every variant. */
25
31
  rowConstruction: 'string-template' | 'dom-ops'
@@ -78,6 +84,11 @@ export interface BranchPlainLoopPlan {
78
84
  * Undefined off → byte-identical (SR8).
79
85
  */
80
86
  profileLoopId?: string
87
+ /**
88
+ * Preamble-patched regions (#2389) — non-empty forces the multi-line
89
+ * renderItem layout, mirroring the top-level plain plan.
90
+ */
91
+ preambleRegions: readonly PreambleRegionPlan[]
81
92
  }
82
93
 
83
94
  export interface BranchCompositeLoopPlan {
@@ -15,7 +15,7 @@ import { buildChainedArrayExpr, varSlotId, wrapLoopParamAsAccessor } from '../..
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 { destructureLoopParam, loopKeyFn, buildChildRefBindings } from '../shared.ts'
18
+ import { destructureLoopParam, loopKeyFn, buildChildRefBindings, buildPreambleRegionPlans } from '../shared.ts'
19
19
  import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
20
20
  import type {
21
21
  BranchCompositeLoopPlan,
@@ -43,14 +43,21 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
43
43
  || loop.bindings.reactiveTexts.length > 0
44
44
  || loop.bindings.conditionals.length > 0
45
45
 
46
+ // flatMap descriptor mode — see buildPlainLoopPlan (build-loop.ts).
47
+ const fm = loop.flatMapClient
46
48
  const plan: BranchPlainLoopPlan = {
47
49
  kind: 'plain',
48
50
  rowConstruction: 'string-template',
49
51
  containerSlotId,
50
52
  containerVar,
51
53
  markerId: loop.markerId,
52
- arrayExpr: buildChainedArrayExpr(loop),
53
- keyFn: loopKeyFn(loop),
54
+ flatMapLeafItem: fm ? true : undefined,
55
+ arrayExpr: fm
56
+ ? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
57
+ : buildChainedArrayExpr(loop),
58
+ keyFn: fm
59
+ ? (fm.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null')
60
+ : loopKeyFn(loop),
54
61
  paramHead,
55
62
  paramUnwrap,
56
63
  indexParam: loop.index || '__idx',
@@ -75,6 +82,7 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
75
82
  : null,
76
83
  eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
77
84
  childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
85
+ preambleRegions: buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings),
78
86
  bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
79
87
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
80
88
  }
@@ -36,11 +36,18 @@ export function buildDynamicLoopDelegationPlan(
36
36
  paramBindings: elem.paramBindings,
37
37
  key: elem.key,
38
38
  index: elem.index,
39
+ // No loopParams spec here (unlike the row-render context) — in the
40
+ // delegated handler `elem.param` is bound to the plain `.find()`/
41
+ // indexed result, not a signal accessor, so leaf refs must stay in
42
+ // their literal (`t.name`) form. Passing a loopParams spec here was
43
+ // BUG-3: it rewrote leaf refs to accessor-call form (`t().name`),
44
+ // which throws since `t` is a plain object in this scope.
39
45
  mapPreamble: elem.preamble
40
46
  ? renderPreamble(elem.preamble, {
41
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
47
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
42
48
  })
43
49
  : null,
50
+ mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
44
51
  }),
45
52
  }
46
53
  }
@@ -65,11 +72,14 @@ export function buildBranchLoopDelegationPlan(
65
72
  paramBindings: loop.paramBindings,
66
73
  key: loop.key,
67
74
  index: loop.index,
75
+ // See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
76
+ // loopParams spec — leaf refs must stay in plain-object form here.
68
77
  mapPreamble: loop.preamble
69
78
  ? renderPreamble(loop.preamble, {
70
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
79
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
71
80
  })
72
81
  : null,
82
+ mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
73
83
  }),
74
84
  }
75
85
  }
@@ -95,11 +105,14 @@ export function buildStaticArrayDelegationPlan(
95
105
  // array too (#1434).
96
106
  arrayExpr: buildChainedArrayExpr(elem),
97
107
  param: elem.param,
108
+ // See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
109
+ // loopParams spec — leaf refs must stay in plain-object form here.
98
110
  mapPreamble: elem.preamble
99
111
  ? renderPreamble(elem.preamble, {
100
- renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
112
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
101
113
  })
102
114
  : null,
115
+ mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
103
116
  offset: elem.offset ?? null,
104
117
  indexParam: elem.index ?? null,
105
118
  },
@@ -117,6 +130,7 @@ function buildKeyedOrIndexLookup(args: {
117
130
  key: string | null
118
131
  index: string | null
119
132
  mapPreamble: string | null
133
+ mapPreambleDeclaredNames: readonly string[]
120
134
  }): ItemLookup {
121
135
  const hasBindings = (args.paramBindings?.length ?? 0) > 0
122
136
  if (args.key !== null) {
@@ -132,6 +146,7 @@ function buildKeyedOrIndexLookup(args: {
132
146
  paramBindings: args.paramBindings,
133
147
  keyWithItem,
134
148
  mapPreamble: args.mapPreamble,
149
+ mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
135
150
  hasBindings,
136
151
  indexParam: args.index,
137
152
  }
@@ -141,6 +156,7 @@ function buildKeyedOrIndexLookup(args: {
141
156
  arrayExpr: args.array,
142
157
  param: args.param,
143
158
  mapPreamble: args.mapPreamble,
159
+ mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
144
160
  hasBindings,
145
161
  indexParam: args.index,
146
162
  }
@@ -35,6 +35,7 @@ import {
35
35
  destructureLoopParam,
36
36
  buildChildRefBindings,
37
37
  buildStaticChildRefBindings,
38
+ buildPreambleRegionPlans,
38
39
  } from '../shared.ts'
39
40
  import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
40
41
  import { buildComponentLoopPlan } from './build-component-loop.ts'
@@ -91,6 +92,40 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
91
92
  || elem.bindings.reactiveTexts.length > 0
92
93
  || elem.bindings.conditionals.length > 0
93
94
 
95
+ // flatMap descriptor mode: reconcile the FLATTENED leaves. The source
96
+ // accessor runs the flatMap body (plain items — per-item signals don't
97
+ // exist yet) producing `({ k, h })` descriptors; the keyFn reads the
98
+ // leaf-declared key with an index fallback. Everything row-template-
99
+ // related is bypassed — the stringifier emits the build-or-patch
100
+ // renderItem instead. Pre-fix this fell into the generic plain shape
101
+ // with the UN-flattened source, a null keyFn, and an EMPTY template
102
+ // (leaf loss at hydration; cloneNode(null) crash on adds).
103
+ if (elem.flatMapClient) {
104
+ return {
105
+ kind: 'plain',
106
+ rowConstruction: 'string-template',
107
+ containerVar: `_${varSlotId(elem.slotId)}`,
108
+ markerId: elem.markerId,
109
+ profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
110
+ arrayExpr: `(${buildChainedArrayExpr(elem)}).flatMap(${elem.flatMapClient.params} => ${elem.flatMapClient.body})`,
111
+ keyFn: elem.flatMapClient.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null',
112
+ paramHead: '__bfD',
113
+ paramUnwrap: '',
114
+ indexParam: '__idx',
115
+ mapPreambleWrapped: '',
116
+ template: '',
117
+ reactiveEffects: null,
118
+ childRefs: [],
119
+ bodyIsMultiRoot: false,
120
+ anchored: false,
121
+ anchorKeyExpr: '__idx',
122
+ flatMapLeafItem: true,
123
+ // flatMap loops carry no `MapCallbackPreamble` (jsx-to-ir.ts drops it
124
+ // for flatMapCallback), so there is nothing to patch here.
125
+ preambleRegions: [],
126
+ }
127
+ }
128
+
94
129
  return {
95
130
  kind: 'plain',
96
131
  rowConstruction: 'string-template',
@@ -116,6 +151,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
116
151
  skeletonPaths: elem.skeletonPaths,
117
152
  reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
118
153
  childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
154
+ preambleRegions: buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings),
119
155
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
120
156
  anchored: elem.bodyIsItemConditional ?? false,
121
157
  // Fall back to the iteration index when the loop has no key. A whole-item
@@ -66,8 +66,19 @@ export interface KeyedItemLookup {
66
66
  * decision.
67
67
  */
68
68
  keyWithItem: string
69
- /** Optional preamble line — emitted before the handler call. */
69
+ /** Optional preamble line — emitted before the handler call, inside the item guard. */
70
70
  mapPreamble: string | null
71
+ /**
72
+ * `const`/`let`/function names `mapPreamble` declares (from
73
+ * `MapCallbackPreamble.declaredNames`). The stringifier only splices
74
+ * `mapPreamble` for an event whose handler text actually references one of
75
+ * these — a preamble that only feeds unused local state (e.g. a `cells`
76
+ * array builder never read by the click handler) is dead weight the
77
+ * delegated handler shouldn't pay for, and emitting it unconditionally is
78
+ * what let BUG-3's stale-context leaf render go unnoticed. Empty when
79
+ * `mapPreamble` is `null`.
80
+ */
81
+ mapPreambleDeclaredNames: readonly string[]
71
82
  /** True when `paramBindings` is non-empty — drives TDZ-safe lookup shape. */
72
83
  hasBindings: boolean
73
84
  }
@@ -77,6 +88,8 @@ export interface DynamicIndexItemLookup {
77
88
  arrayExpr: string
78
89
  param: string
79
90
  mapPreamble: string | null
91
+ /** See `KeyedItemLookup.mapPreambleDeclaredNames`. */
92
+ mapPreambleDeclaredNames: readonly string[]
80
93
  hasBindings: boolean
81
94
  /** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
82
95
  indexParam: string | null
@@ -87,6 +100,8 @@ export interface StaticIndexItemLookup {
87
100
  arrayExpr: string
88
101
  param: string
89
102
  mapPreamble: string | null
103
+ /** See `KeyedItemLookup.mapPreambleDeclaredNames`. */
104
+ mapPreambleDeclaredNames: readonly string[]
90
105
  /** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
91
106
  indexParam: string | null
92
107
  /**
@@ -78,6 +78,19 @@ interface DynamicLoopCommon extends LoopPlanCommon {
78
78
  paramUnwrap: string
79
79
  }
80
80
 
81
+ /**
82
+ * A resolved preamble-patched region (#2389), ready to emit: `valueExpr` is
83
+ * already wrapped with the loop-param accessor and, for a `joinArrayChild`
84
+ * source, already carries the array-join ternary — the exact value
85
+ * `irToHtmlTemplate` computes for the same node in the row template, so a
86
+ * same-key update's re-patch stays byte-identical to what a fresh mount
87
+ * would render.
88
+ */
89
+ export interface PreambleRegionPlan {
90
+ slotId: string
91
+ valueExpr: string
92
+ }
93
+
81
94
  /** Per-item ref callback resolved against a child slot for emission (#1244). */
82
95
  export interface LoopChildRefBinding {
83
96
  /** bf slot ID of the target element (root or descendant of the body). */
@@ -146,6 +159,21 @@ interface PlainLoopVariant extends DynamicLoopCommon {
146
159
  * Empty when the loop has no key (only meaningful when `anchored`).
147
160
  */
148
161
  anchorKeyExpr: string
162
+ /**
163
+ * flatMap descriptor mode (`LoopCore.flatMapClient`): `arrayExpr` already
164
+ * includes the `.flatMap(params => descriptorBody)` projection — the
165
+ * runtime reconciles the FLATTENED `({ k, h })` leaves — and `keyFn` keys
166
+ * on `d.k` (index fallback). The stringifier emits the build-or-patch
167
+ * renderItem (`patchLeaf`) instead of the template-clone shape; `template`
168
+ * is empty and `mapPreambleWrapped` unused in this mode.
169
+ */
170
+ flatMapLeafItem?: boolean
171
+ /**
172
+ * Preamble-patched regions (#2389) — non-empty forces the multi-line
173
+ * renderItem layout (mirrors `childRefs`) so the region-patch effects have
174
+ * `__el` to query against.
175
+ */
176
+ preambleRegions: readonly PreambleRegionPlan[]
149
177
  }
150
178
 
151
179
  /**
@@ -20,7 +20,8 @@ import { irChildrenToJsExpr } from '../html-template.ts'
20
20
  import { emitListenerBlock } from './stringify/event-listener.ts'
21
21
  import { nameForRegistryRef } from '../component-scope.ts'
22
22
  import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
23
- import type { LoopChildRefBinding } from './plan/loop.ts'
23
+ import type { LoopChildRefBinding, PreambleRegionPlan } from './plan/loop.ts'
24
+ import type { PreambleRegionSource } from '../../types.ts'
24
25
  import {
25
26
  extractFreeIdentifiersFromText,
26
27
  extractFreeIdentifiersFromStatementText,
@@ -237,6 +238,30 @@ export function destructureLoopParam(
237
238
  return { head: param, unwrap: '' }
238
239
  }
239
240
 
241
+ /**
242
+ * Resolve `IRLoop.preambleRegions` (#2389) into ready-to-emit
243
+ * `PreambleRegionPlan`s: wrap each source expression with the loop-param
244
+ * accessor and, for a `joinArrayChild` source, apply the same array-join
245
+ * ternary `irToHtmlTemplate` uses for the identical node in the row
246
+ * template — so the region-patch effect's re-render matches what a fresh
247
+ * mount would have produced. Shared by the top-level (`build-loop.ts`) and
248
+ * branch (`build-branch-loop.ts`) plain-loop-plan builders.
249
+ */
250
+ export function buildPreambleRegionPlans(
251
+ regions: readonly PreambleRegionSource[] | undefined,
252
+ loopParam: string,
253
+ loopParamBindings: readonly LoopParamBinding[] | undefined,
254
+ ): readonly PreambleRegionPlan[] {
255
+ if (!regions || regions.length === 0) return []
256
+ return regions.map((r) => {
257
+ const wrapped = wrapLoopParamAsAccessor(r.expr, loopParam, loopParamBindings)
258
+ const valueExpr = r.joinArrayChild
259
+ ? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
260
+ : `escapeText(${wrapped})`
261
+ return { slotId: r.slotId, valueExpr }
262
+ })
263
+ }
264
+
240
265
  /**
241
266
  * Build a props object expression string from component prop definitions.
242
267
  * Shared by emitComponentLoopReconciliation and emitCompositeElementReconciliation.
@@ -14,7 +14,7 @@ import { stringifyCompositeLoop } from './composite-loop.ts'
14
14
  import { stringifyEventDelegation } from './event-delegation.ts'
15
15
  import { stringifyReactiveEffects } from './reactive-effects.ts'
16
16
  import { emitTemplateCloneInline, emitLoopItemElementSetup } from './template-parse.ts'
17
- import { emitLoopChildRefs } from './loop.ts'
17
+ import { emitLoopChildRefs, emitPreambleRegionEffects } from './loop.ts'
18
18
  import type {
19
19
  BranchLoopPlan,
20
20
  BranchPlainLoopPlan,
@@ -58,6 +58,7 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
58
58
  childRefs,
59
59
  bodyIsMultiRoot,
60
60
  profileLoopId,
61
+ preambleRegions,
61
62
  } = plan
62
63
 
63
64
  const loopBfId = profileLoopId ? `, ${JSON.stringify(profileLoopId)}` : ''
@@ -69,9 +70,29 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
69
70
  // both the effect and its dependency subscriptions (observation O-2).
70
71
  lines.push(` __disposers.push(createDisposableEffect(() => {`)
71
72
 
73
+ // flatMap descriptor mode — build-or-patch renderItem, mirrors the
74
+ // top-level shape in stringify/loop.ts (see comment there).
75
+ if (plan.flatMapLeafItem) {
76
+ lines.push(` if (${containerVar}) mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (__bfD, ${indexParam}, __existing) => {`)
77
+ lines.push(` let __el = __existing`)
78
+ lines.push(` if (!__el) { const __tpl = document.createElement('template'); __tpl.innerHTML = __bfD().h; __el = __tpl.content.firstElementChild }`)
79
+ lines.push(` let __last = __existing ? undefined : __bfD().h`)
80
+ lines.push(` createEffect(() => {`)
81
+ lines.push(` const __html = __bfD().h`)
82
+ lines.push(` if (__last === undefined) { __last = __html; return }`)
83
+ lines.push(` if (__html !== __last) { __last = __html; patchLeaf(__el, __html) }`)
84
+ lines.push(` })`)
85
+ lines.push(` return __el`)
86
+ lines.push(` }, '${markerId}'${loopBfId})`)
87
+ lines.push(` }))`)
88
+ stringifyEventDelegation(lines, eventDelegation)
89
+ return
90
+ }
91
+
72
92
  // Non-empty `childRefs` need `__el` as a handle inside the factory body,
73
- // so force the multi-line layout (#1244).
74
- if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0) {
93
+ // so force the multi-line layout (#1244) — preamble-patched regions
94
+ // (#2389) need the same handle for their own effect.
95
+ if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0 && preambleRegions.length === 0) {
75
96
  // Simple case: single-line renderItem (single root, no reactive effects).
76
97
  const cloneExpr = emitTemplateCloneInline(template)
77
98
  if (mapPreambleWrapped) {
@@ -98,6 +119,7 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
98
119
  stringifyReactiveEffects(lines, reactiveEffects, { indent: ' ', elVar: '__el', bodyIsMultiRoot })
99
120
  }
100
121
  emitLoopChildRefs(lines, childRefs, { indent: ' ', elVar: '__el', bodyIsMultiRoot })
122
+ emitPreambleRegionEffects(lines, preambleRegions, mapPreambleWrapped, { indent: ' ', elVar: '__el' })
101
123
  lines.push(` return __el`)
102
124
  lines.push(` }, '${markerId}'${loopBfId})`)
103
125
  }