@barefootjs/jsx 0.26.2 → 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 (98) hide show
  1. package/dist/adapters/interface.d.ts +29 -0
  2. package/dist/adapters/interface.d.ts.map +1 -1
  3. package/dist/adapters/jsx-adapter.d.ts +9 -0
  4. package/dist/adapters/jsx-adapter.d.ts.map +1 -1
  5. package/dist/adapters/loop-bound-names.d.ts.map +1 -1
  6. package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
  7. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  8. package/dist/adapters/test-adapter.d.ts.map +1 -1
  9. package/dist/analyzer-context.d.ts +11 -1
  10. package/dist/analyzer-context.d.ts.map +1 -1
  11. package/dist/analyzer.d.ts +40 -1
  12. package/dist/analyzer.d.ts.map +1 -1
  13. package/dist/expression-parser.d.ts.map +1 -1
  14. package/dist/index.js +1086 -269
  15. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts +14 -1
  18. package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
  26. package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +39 -0
  28. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/control-flow/shared.d.ts +12 -1
  30. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  31. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts +1 -1
  32. package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
  33. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -0
  34. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  35. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  36. package/dist/ir-to-client-js/html-template.d.ts +67 -1
  37. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  38. package/dist/ir-to-client-js/imports.d.ts +2 -2
  39. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  40. package/dist/ir-to-client-js/plan/build-static-array-child-init.d.ts.map +1 -1
  41. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  42. package/dist/ir-to-client-js/types.d.ts +29 -4
  43. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  44. package/dist/jsx-to-ir.d.ts.map +1 -1
  45. package/dist/loop-destructure.d.ts.map +1 -1
  46. package/dist/strip-types.d.ts +18 -0
  47. package/dist/strip-types.d.ts.map +1 -1
  48. package/dist/types.d.ts +143 -32
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +277 -33
  52. package/src/__tests__/client-js-generation.test.ts +18 -5
  53. package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
  54. package/src/__tests__/compiler-stress-1244.test.ts +12 -1
  55. package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
  56. package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
  57. package/src/__tests__/flatmap-segments.test.ts +262 -0
  58. package/src/__tests__/map-arbitrary-body.test.ts +226 -0
  59. package/src/__tests__/map-body-no-silent-divergence.test.ts +405 -0
  60. package/src/__tests__/map-multi-return-body.test.ts +172 -0
  61. package/src/__tests__/preamble-region-patch.test.ts +150 -0
  62. package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
  63. package/src/__tests__/unsupported-expression.test.ts +20 -2
  64. package/src/adapters/interface.ts +40 -0
  65. package/src/adapters/jsx-adapter.ts +10 -0
  66. package/src/adapters/loop-bound-names.ts +6 -2
  67. package/src/adapters/parsed-expr-emitter.ts +5 -10
  68. package/src/adapters/test-adapter.ts +10 -0
  69. package/src/analyzer-context.ts +35 -1
  70. package/src/analyzer.ts +162 -24
  71. package/src/compiler.ts +2 -2
  72. package/src/expression-parser.ts +27 -19
  73. package/src/ir-to-client-js/build-references.ts +19 -2
  74. package/src/ir-to-client-js/collect-elements.ts +69 -13
  75. package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +14 -1
  76. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +18 -5
  77. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
  78. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
  79. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +32 -3
  80. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
  81. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +53 -2
  82. package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
  83. package/src/ir-to-client-js/control-flow/plan/loop.ts +40 -0
  84. package/src/ir-to-client-js/control-flow/shared.ts +28 -2
  85. package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +25 -3
  86. package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +76 -21
  87. package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
  88. package/src/ir-to-client-js/control-flow.ts +11 -0
  89. package/src/ir-to-client-js/html-template.ts +244 -39
  90. package/src/ir-to-client-js/imports.ts +1 -1
  91. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
  92. package/src/ir-to-client-js/reactivity.ts +6 -0
  93. package/src/ir-to-client-js/types.ts +28 -3
  94. package/src/jsx-to-ir.ts +1059 -167
  95. package/src/loop-destructure.ts +9 -5
  96. package/src/rich-type-refusal.ts +6 -2
  97. package/src/strip-types.ts +47 -0
  98. package/src/types.ts +159 -35
@@ -60,6 +60,10 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
60
60
 
61
61
  return {
62
62
  kind: 'component',
63
+ // Rows are createComponent-driven: a lowered HTML-string leaf passed as a
64
+ // prop would diverge from SSR (which passes real JSX elements). The plan
65
+ // dispatcher refuses a JSX-bearing preamble on this variant.
66
+ rowConstruction: 'dom-ops',
63
67
  containerVar: `_${varSlotId(elem.slotId)}`,
64
68
  markerId: elem.markerId,
65
69
  arrayExpr: buildChainedArrayExpr(elem),
@@ -23,6 +23,7 @@ import {
23
23
  } from '../shared.ts'
24
24
  import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
25
25
  import { buildInnerLoopsPlan } from './build-inner-loop.ts'
26
+ import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
26
27
  import type { CompositeLoopPlan } from './types.ts'
27
28
 
28
29
  /** @internal — prefer `buildLoopPlan`. */
@@ -36,6 +37,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
36
37
 
37
38
  return {
38
39
  kind: 'composite',
40
+ rowConstruction: 'string-template',
39
41
  containerVar: `_${varSlotId(elem.slotId)}`,
40
42
  markerId: elem.markerId,
41
43
  arrayExpr: buildChainedArrayExpr(elem),
@@ -43,7 +45,12 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
43
45
  paramHead,
44
46
  paramUnwrap,
45
47
  indexParam: elem.index || '__idx',
46
- mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : '',
48
+ mapPreambleWrapped: elem.preamble
49
+ ? renderPreamble(elem.preamble, {
50
+ transformJs: wrap,
51
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
52
+ })
53
+ : '',
47
54
  template: elem.template,
48
55
  outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
49
56
  outerEvents: elem.bindings.events.filter(ev => ev.nestedLoops.length === 0),
@@ -87,6 +94,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
87
94
 
88
95
  return {
89
96
  kind: 'composite',
97
+ rowConstruction: 'string-template',
90
98
  containerVar: `__loop_${cv}`,
91
99
  markerId: loop.markerId,
92
100
  // Chain `.filter()` / `.toSorted()` onto the source array so the mapArray
@@ -97,7 +105,12 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
97
105
  paramHead,
98
106
  paramUnwrap,
99
107
  indexParam: loop.index || '__idx',
100
- mapPreambleWrapped: loop.mapPreamble ? wrap(loop.mapPreamble) : '',
108
+ mapPreambleWrapped: loop.preamble
109
+ ? renderPreamble(loop.preamble, {
110
+ transformJs: wrap,
111
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
112
+ })
113
+ : '',
101
114
  template: loop.template,
102
115
  outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
103
116
  outerEvents: childEvents.filter(ev => ev.nestedLoops.length === 0),
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { TopLevelLoop, BranchLoop, LoopChildEvent } from '../../types.ts'
13
13
  import { buildChainedArrayExpr, varSlotId, substituteLoopBindings } from '../../utils.ts'
14
+ import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
14
15
  import type {
15
16
  EventDelegationPlan,
16
17
  ItemLookup,
@@ -35,7 +36,18 @@ export function buildDynamicLoopDelegationPlan(
35
36
  paramBindings: elem.paramBindings,
36
37
  key: elem.key,
37
38
  index: elem.index,
38
- mapPreamble: elem.mapPreamble ?? null,
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.
45
+ mapPreamble: elem.preamble
46
+ ? renderPreamble(elem.preamble, {
47
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
48
+ })
49
+ : null,
50
+ mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
39
51
  }),
40
52
  }
41
53
  }
@@ -60,7 +72,14 @@ export function buildBranchLoopDelegationPlan(
60
72
  paramBindings: loop.paramBindings,
61
73
  key: loop.key,
62
74
  index: loop.index,
63
- mapPreamble: loop.mapPreamble ?? null,
75
+ // See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
76
+ // loopParams spec — leaf refs must stay in plain-object form here.
77
+ mapPreamble: loop.preamble
78
+ ? renderPreamble(loop.preamble, {
79
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
80
+ })
81
+ : null,
82
+ mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
64
83
  }),
65
84
  }
66
85
  }
@@ -86,7 +105,14 @@ export function buildStaticArrayDelegationPlan(
86
105
  // array too (#1434).
87
106
  arrayExpr: buildChainedArrayExpr(elem),
88
107
  param: elem.param,
89
- mapPreamble: elem.mapPreamble ?? null,
108
+ // See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
109
+ // loopParams spec — leaf refs must stay in plain-object form here.
110
+ mapPreamble: elem.preamble
111
+ ? renderPreamble(elem.preamble, {
112
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
113
+ })
114
+ : null,
115
+ mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
90
116
  offset: elem.offset ?? null,
91
117
  indexParam: elem.index ?? null,
92
118
  },
@@ -104,6 +130,7 @@ function buildKeyedOrIndexLookup(args: {
104
130
  key: string | null
105
131
  index: string | null
106
132
  mapPreamble: string | null
133
+ mapPreambleDeclaredNames: readonly string[]
107
134
  }): ItemLookup {
108
135
  const hasBindings = (args.paramBindings?.length ?? 0) > 0
109
136
  if (args.key !== null) {
@@ -119,6 +146,7 @@ function buildKeyedOrIndexLookup(args: {
119
146
  paramBindings: args.paramBindings,
120
147
  keyWithItem,
121
148
  mapPreamble: args.mapPreamble,
149
+ mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
122
150
  hasBindings,
123
151
  indexParam: args.index,
124
152
  }
@@ -128,6 +156,7 @@ function buildKeyedOrIndexLookup(args: {
128
156
  arrayExpr: args.array,
129
157
  param: args.param,
130
158
  mapPreamble: args.mapPreamble,
159
+ mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
131
160
  hasBindings,
132
161
  indexParam: args.index,
133
162
  }
@@ -29,6 +29,7 @@ import {
29
29
  attrValueToString,
30
30
  } from '../../utils.ts'
31
31
  import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
32
+ import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
32
33
 
33
34
  /**
34
35
  * Mirror of the helper in `build-loop-child-arm.ts` — kept local to avoid
@@ -115,7 +116,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
115
116
  const useReactive = refsParent && !!inner.template
116
117
 
117
118
  const emit: InnerLoopReactiveEmit | InnerLoopStaticEmit = useReactive
118
- ? buildReactiveEmit(inner, level, wrapOuter, uidSuffix)
119
+ ? buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings)
119
120
  : buildStaticEmit(inner, level, uidSuffix)
120
121
 
121
122
  const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array
@@ -155,6 +156,8 @@ function buildReactiveEmit(
155
156
  level: DepthLevel,
156
157
  wrapOuter: (expr: string) => string,
157
158
  uidSuffix: string,
159
+ outerLoopParam?: string,
160
+ outerLoopParamBindings?: readonly LoopParamBinding[],
158
161
  ): InnerLoopReactiveEmit {
159
162
  const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
160
163
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
@@ -229,7 +232,20 @@ function buildReactiveEmit(
229
232
  const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, paramHead, level.comps, level.events)
230
233
  if (indexAlias) preludeStatements.push(indexAlias)
231
234
  if (paramUnwrap) preludeStatements.push(paramUnwrap)
232
- if (inner.mapPreamble) preludeStatements.push(wrapInner(wrapOuter(inner.mapPreamble)))
235
+ if (inner.preamble) {
236
+ // Leaf JSX renders under both param contexts, mirroring the
237
+ // wrapInner(wrapOuter(...)) applied to the js text.
238
+ const leafLoopParams = outerLoopParam
239
+ ? [
240
+ { param: outerLoopParam, bindings: outerLoopParamBindings },
241
+ { param: inner.param, bindings: inner.paramBindings },
242
+ ]
243
+ : [{ param: inner.param, bindings: inner.paramBindings }]
244
+ preludeStatements.push(renderPreamble(inner.preamble, {
245
+ transformJs: (t) => wrapInner(wrapOuter(t)),
246
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined, true),
247
+ }))
248
+ }
233
249
 
234
250
  const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
235
251
 
@@ -265,7 +281,14 @@ function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string
265
281
  const preludeStatements: string[] = []
266
282
  const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events)
267
283
  if (indexAlias) preludeStatements.push(indexAlias)
268
- if (inner.mapPreamble) preludeStatements.push(inner.mapPreamble)
284
+ // Static `forEach` receives the literal item, so neither the js text nor a
285
+ // leaf's param reads get accessor-wrapped (mirrors the verbatim emission
286
+ // of the js segments above).
287
+ if (inner.preamble) {
288
+ preludeStatements.push(renderPreamble(inner.preamble, {
289
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
290
+ }))
291
+ }
269
292
  return {
270
293
  mode: 'static',
271
294
  rawKey: inner.key ?? null,
@@ -35,10 +35,12 @@ 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'
41
42
  import { buildTopLevelCompositePlan } from './build-composite-loop.ts'
43
+ import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
42
44
  import type {
43
45
  LoopPlan,
44
46
  PlainLoopPlan,
@@ -90,8 +92,43 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
90
92
  || elem.bindings.reactiveTexts.length > 0
91
93
  || elem.bindings.conditionals.length > 0
92
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
+
93
129
  return {
94
130
  kind: 'plain',
131
+ rowConstruction: 'string-template',
95
132
  containerVar: `_${varSlotId(elem.slotId)}`,
96
133
  markerId: elem.markerId,
97
134
  profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
@@ -100,12 +137,21 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
100
137
  paramHead,
101
138
  paramUnwrap,
102
139
  indexParam: elem.index || '__idx',
103
- mapPreambleWrapped: elem.mapPreamble ? wrap(elem.mapPreamble) : '',
140
+ // Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
141
+ // render as HTML-string templates under this loop's param context so a
142
+ // leaf that reads the item (`r`) becomes `r()`.
143
+ mapPreambleWrapped: elem.preamble
144
+ ? renderPreamble(elem.preamble, {
145
+ transformJs: wrap,
146
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
147
+ })
148
+ : '',
104
149
  template: elem.template,
105
150
  skeletonTemplate: elem.skeletonTemplate,
106
151
  skeletonPaths: elem.skeletonPaths,
107
152
  reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
108
153
  childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
154
+ preambleRegions: buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings),
109
155
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
110
156
  anchored: elem.bodyIsItemConditional ?? false,
111
157
  // Fall back to the iteration index when the loop has no key. A whole-item
@@ -139,6 +185,7 @@ export function buildStaticLoopPlan(elem: TopLevelLoop, unsafeLocalNames: Set<st
139
185
 
140
186
  return {
141
187
  kind: 'static',
188
+ rowConstruction: 'string-template',
142
189
  containerVar: `_${varSlotId(elem.slotId)}`,
143
190
  arrayExpr: elem.array,
144
191
  param: elem.param,
@@ -188,7 +235,11 @@ function buildStaticLoopMaterialize(
188
235
  if (!setIntersects(elem.arrayFreeIdentifiers, unsafeLocalNames)) return null
189
236
  return {
190
237
  itemTemplate: elem.staticItemTemplate,
191
- mapPreamble: elem.mapPreamble ?? '',
238
+ mapPreamble: elem.preamble
239
+ ? renderPreamble(elem.preamble, {
240
+ renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
241
+ })
242
+ : '',
192
243
  bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
193
244
  }
194
245
  }
@@ -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
  /**
@@ -45,6 +45,18 @@ interface LoopPlanCommon {
45
45
  * — initial mount, SSR hydration, and same-key remount after unmount.
46
46
  */
47
47
  childRefs: readonly LoopChildRefBinding[]
48
+ /**
49
+ * How this variant constructs each row's DOM (Stage 3 root cure,
50
+ * spec/callback-fidelity.md). `'string-template'` = rows are built from
51
+ * interpolated innerHTML template literals, so a `.map()` preamble carrying
52
+ * lowered JSX leaves can be spliced via `renderPreamble()`. `'dom-ops'` =
53
+ * rows are built imperatively (createElement/reconcile paths) and CANNOT
54
+ * host a string-lowered preamble — the plan dispatcher refuses a JSX-bearing
55
+ * preamble on such a variant instead of letting it drop silently. Required
56
+ * on every variant (like `childRefs`) so a new loop shape must declare its
57
+ * capability — the compile error is the point.
58
+ */
59
+ rowConstruction: 'string-template' | 'dom-ops'
48
60
  }
49
61
 
50
62
  /** Fields shared by every dynamic (`mapArray`-driven) loop variant. */
@@ -66,6 +78,19 @@ interface DynamicLoopCommon extends LoopPlanCommon {
66
78
  paramUnwrap: string
67
79
  }
68
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
+
69
94
  /** Per-item ref callback resolved against a child slot for emission (#1244). */
70
95
  export interface LoopChildRefBinding {
71
96
  /** bf slot ID of the target element (root or descendant of the body). */
@@ -134,6 +159,21 @@ interface PlainLoopVariant extends DynamicLoopCommon {
134
159
  * Empty when the loop has no key (only meaningful when `anchored`).
135
160
  */
136
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[]
137
177
  }
138
178
 
139
179
  /**
@@ -14,12 +14,14 @@
14
14
 
15
15
  import type { LoopChildEvent, LoopChildRef, TopLevelLoop, NestedLoop, CollectedLoop } from '../types.ts'
16
16
  import type { IRLoopChildComponent, LoopParamBinding } from '../../types.ts'
17
+ import { preambleAnalysisText } from '../../types.ts'
17
18
  import { quotePropName, wrapLoopParamAsAccessor, irChildrenFreeIds, attrValueToString } from '../utils.ts'
18
19
  import { irChildrenToJsExpr } from '../html-template.ts'
19
20
  import { emitListenerBlock } from './stringify/event-listener.ts'
20
21
  import { nameForRegistryRef } from '../component-scope.ts'
21
22
  import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
22
- import type { LoopChildRefBinding } from './plan/loop.ts'
23
+ import type { LoopChildRefBinding, PreambleRegionPlan } from './plan/loop.ts'
24
+ import type { PreambleRegionSource } from '../../types.ts'
23
25
  import {
24
26
  extractFreeIdentifiersFromText,
25
27
  extractFreeIdentifiersFromStatementText,
@@ -83,7 +85,7 @@ export function nestedLoopReferencesIndex(
83
85
  for (const a of inner.bindings.reactiveAttrs) {
84
86
  if (exprRefs(a.expression, a.freeIdentifiers)) return true
85
87
  }
86
- if (inner.mapPreamble && extractFreeIdentifiersFromStatementText(inner.mapPreamble).has(index)) return true
88
+ if (inner.preamble && extractFreeIdentifiersFromStatementText(preambleAnalysisText(inner.preamble)).has(index)) return true
87
89
  if (inner.template && extractFreeIdentifiersFromTemplateText(inner.template).has(index)) return true
88
90
  for (const ev of events) {
89
91
  if (exprRefs(ev.handler)) return true
@@ -236,6 +238,30 @@ export function destructureLoopParam(
236
238
  return { head: param, unwrap: '' }
237
239
  }
238
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
+
239
265
  /**
240
266
  * Build a props object expression string from component prop definitions.
241
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
  }