@barefootjs/jsx 0.29.0 → 0.30.2

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 (90) hide show
  1. package/dist/adapters/child-scope.d.ts +17 -0
  2. package/dist/adapters/child-scope.d.ts.map +1 -0
  3. package/dist/adapters/loop-bound-names.d.ts.map +1 -1
  4. package/dist/analyzer-context.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +990 -367
  9. package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
  10. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
  12. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +92 -0
  15. package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
  16. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
  17. package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
  18. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
  19. package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
  21. package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/html-template.d.ts +2 -3
  25. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
  27. package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/reactivity.d.ts +1 -10
  29. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/types.d.ts +12 -0
  31. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  32. package/dist/jsx-to-ir.d.ts.map +1 -1
  33. package/dist/prop-rewrite.d.ts +24 -7
  34. package/dist/prop-rewrite.d.ts.map +1 -1
  35. package/dist/rich-type-evidence.d.ts.map +1 -1
  36. package/dist/ssr-defaults.d.ts.map +1 -1
  37. package/dist/to-locale-date-lowering.d.ts.map +1 -1
  38. package/dist/types.d.ts +78 -0
  39. package/dist/types.d.ts.map +1 -1
  40. package/package.json +2 -2
  41. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
  42. package/src/__tests__/branch-loop-plain.test.ts +28 -45
  43. package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
  44. package/src/__tests__/csr-template-scope-soundness.test.ts +167 -0
  45. package/src/__tests__/early-return-branch-switch.test.ts +96 -0
  46. package/src/__tests__/form-control-value-ssr.test.ts +102 -0
  47. package/src/__tests__/lazy-conditional.test.ts +190 -0
  48. package/src/__tests__/lazy-preamble.test.ts +297 -0
  49. package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
  50. package/src/__tests__/loop-item-root-scope.test.ts +213 -0
  51. package/src/__tests__/map-multi-return-body.test.ts +30 -3
  52. package/src/__tests__/nested-loop-conditional.test.ts +27 -6
  53. package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
  54. package/src/__tests__/preamble-declarations.test.ts +207 -0
  55. package/src/__tests__/ssr-defaults.test.ts +44 -0
  56. package/src/adapters/child-scope.ts +22 -0
  57. package/src/adapters/loop-bound-names.ts +6 -0
  58. package/src/analyzer-context.ts +43 -0
  59. package/src/analyzer.ts +33 -3
  60. package/src/index.ts +1 -0
  61. package/src/ir-to-client-js/collect-elements.ts +51 -10
  62. package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
  63. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
  64. package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
  65. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
  66. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
  67. package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -2
  68. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
  69. package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +151 -0
  70. package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
  71. package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
  72. package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
  73. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
  74. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
  75. package/src/ir-to-client-js/csr-substitute.ts +6 -1
  76. package/src/ir-to-client-js/emit-registration.ts +1 -1
  77. package/src/ir-to-client-js/html-template.ts +51 -40
  78. package/src/ir-to-client-js/index.ts +1 -1
  79. package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
  80. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
  81. package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
  82. package/src/ir-to-client-js/reactivity.ts +25 -0
  83. package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
  84. package/src/ir-to-client-js/types.ts +12 -0
  85. package/src/jsx-to-ir.ts +409 -24
  86. package/src/prop-rewrite.ts +149 -19
  87. package/src/rich-type-evidence.ts +6 -7
  88. package/src/ssr-defaults.ts +7 -2
  89. package/src/to-locale-date-lowering.ts +8 -2
  90. package/src/types.ts +80 -0
@@ -70,7 +70,12 @@ import {
70
70
  emitTemplateCloneInline,
71
71
  hoistedCloneExpr,
72
72
  } from './template-parse.ts'
73
- import type { LazyRowAttrBinding, LazyRowPlanData, LazyRowTextBinding } from '../plan/build-lazy-row.ts'
73
+ import type {
74
+ LazyRowAttrBinding,
75
+ LazyRowConditionalBinding,
76
+ LazyRowPlanData,
77
+ LazyRowTextBinding,
78
+ } from '../plan/build-lazy-row.ts'
74
79
 
75
80
  export interface StringifyLazyRowOptions {
76
81
  /** Indent of the `mapArrayLazy(` line itself. */
@@ -94,9 +99,11 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
94
99
  const { indent, lazyRow, paramHead } = o
95
100
  const mid = o.markerId.replace(/[^A-Za-z0-9_$]/g, '_')
96
101
  const tplVar = `__tpl_${mid}`
97
- const claimVar = `__lzc_${mid}`
98
102
  const hasRefs = lazyRow.attrSlotIds.length > 0 || lazyRow.texts.length > 0
99
- const hasBindings = lazyRow.attrs.length > 0 || lazyRow.texts.length > 0
103
+ // Conditionals count: `createRow` seeds their dedup boolean into `__l`, so a
104
+ // row whose ONLY reactive thing is a conditional still needs `__l` declared.
105
+ const hasBindings =
106
+ lazyRow.attrs.length > 0 || lazyRow.texts.length > 0 || lazyRow.conditionals.length > 0
100
107
  // ONE per-loop door decision (see `refParts`): the read-capable claim, or
101
108
  // today's write-only writer. Never decided per binding — the door is
102
109
  // allocated once per ROW.
@@ -165,28 +172,17 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
165
172
  }
166
173
  }
167
174
 
168
- // Lazy ref claim for ADOPTED (SSR) rows: one scan inside that row, cached
169
- // on `entry.refs`. Shared by applyItem and applyOuter so a row claims once.
170
- //
171
- // The text door slot is left EMPTY here and filled by the first content
172
- // write (`doorAccess`). An adopted row is claimed by whichever of
173
- // applyItem/applyOuter touches it first, and an `applyOuter` that only
174
- // drives ATTRIBUTES never reads or writes a content slot so building the
175
- // door in this function spends one closure per row of the list on something
176
- // that stays unused until (and unless) that row's item changes. Deferring
177
- // is confined to the adopted path on purpose: `createRow` writes every text
178
- // immediately, so its door is used on the same tick it is built and there
179
- // is nothing to defer (see the `deferDoor` note on `refParts`).
180
- if (hasRefs) {
181
- // With the door deferred, the ELEMENT refs are the only parts that read the
182
- // row root — so a text-only row (no reactive-attr slot) claims to a bare
183
- // `[null]` and the `__el` binding would be dead. `parts` decides, rather
184
- // than a second predicate that could drift from `refParts`.
185
- const parts = refParts(lazyRow, '__el', null, adoptedPlanVar, true)
186
- lines.push(`${indent}const ${claimVar} = (__e) => {`)
187
- if (parts.some(p => p.includes('__el'))) lines.push(`${indent} const __el = __e.primaryEl`)
188
- lines.push(`${indent} return [${parts.join(', ')}]`)
189
- lines.push(`${indent}}`)
175
+ // Hoisted arm templates, one pair per conditional. Both arms are static by
176
+ // construction (`analyzeLazyConditional`), so each is parsed ONCE per loop and
177
+ // cloned on a flip — a per-row `innerHTML` parse would undo the point of
178
+ // driving the swap from the loop level. The comparison the seed run makes is
179
+ // against `content.firstElementChild.outerHTML`, i.e. the browser's OWN
180
+ // serialization of the same markup, so it cannot false-mismatch on attribute
181
+ // order or quoting the way comparing against the authored string would.
182
+ for (const c of lazyRow.conditionals) {
183
+ const v = condVars(mid, c.slotId)
184
+ emitHoistedTemplateDecl(lines, indent, v.trueTpl, c.whenTrueHtml)
185
+ emitHoistedTemplateDecl(lines, indent, v.falseTpl, c.whenFalseHtml)
190
186
  }
191
187
 
192
188
  const call = `mapArrayLazy(() => ${o.arrayExpr}, ${o.containerVar}, ${o.keyFn}, {`
@@ -200,6 +196,9 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
200
196
  // interpolates the per-row template, which reads the param for at least
201
197
  // the `data-key` attribute.
202
198
  lines.push(`${b2}const ${paramHead} = () => __e.item`)
199
+ // The row's `.map()` callback preamble, before the clone: a non-hoisted
200
+ // per-row template interpolates values the preamble declares.
201
+ if (lazyRow.preambleStatements) lines.push(`${b2}${lazyRow.preambleStatements}`)
203
202
  const cloneExpr = useHoisted
204
203
  ? hoistedCloneExpr(tplVar, o.skeletonTemplate!)
205
204
  : `(() => { ${emitTemplateCloneInline(o.template)} })()`
@@ -213,31 +212,42 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
213
212
  const createDoor = `__r[${lazyRow.writerIndex}]`
214
213
  for (const t of lazyRow.texts) emitTextBinding(lines, b2, t, createDoor, 'create', rwDoor)
215
214
  }
215
+ for (const c of lazyRow.conditionals) emitConditional(lines, b2, mid, c, 'create')
216
216
  lines.push(`${b2}return __el`)
217
217
  lines.push(`${b1}},`)
218
218
 
219
219
  // --- applyItem ---------------------------------------------------------
220
220
  const itemAttrs = lazyRow.attrs.filter(a => a.readsItem)
221
221
  const itemTexts = lazyRow.texts.filter(t => t.readsItem)
222
- if (itemAttrs.length === 0 && itemTexts.length === 0) {
222
+ const itemConds = lazyRow.conditionals.filter(c => c.readsItem)
223
+ if (itemAttrs.length === 0 && itemTexts.length === 0 && itemConds.length === 0) {
223
224
  lines.push(`${b1}applyItem: () => {},`)
224
225
  } else {
225
226
  lines.push(`${b1}applyItem: (__e) => {`)
226
227
  lines.push(`${b2}const ${paramHead} = () => __e.item`)
227
- lines.push(`${b2}const __r = __e.refs ?? (__e.refs = ${claimVar}(__e))`)
228
+ lines.push(`${b2}const __r = __e.refs ?? (__e.refs = [])`)
228
229
  lines.push(`${b2}const __l = __e.last ?? (__e.last = [])`)
230
+ // Re-run the preamble only when a binding in THIS body reads a local it
231
+ // declares (#2447 follow-up). `mapArrayLazy` wraps `applyItem` in
232
+ // `untrack()`, so a signal read inside the preamble subscribes nothing
233
+ // here — the subscription is `applyOuter`'s job.
234
+ if (lazyRow.itemNeedsPreamble && lazyRow.preambleStatements) {
235
+ lines.push(`${b2}${lazyRow.preambleStatements}`)
236
+ }
229
237
  for (const a of itemAttrs) emitAttrBinding(lines, b2, a, 'item')
230
238
  if (itemTexts.length > 0) {
231
239
  lines.push(`${b2}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
232
240
  for (const t of itemTexts) emitTextBinding(lines, b2, t, '__d', 'item', rwDoor)
233
241
  }
242
+ for (const c of itemConds) emitConditional(lines, b2, mid, c, 'item')
234
243
  lines.push(`${b1}},`)
235
244
  }
236
245
 
237
246
  // --- applyOuter (only when some binding is outer-involving) -------------
238
247
  const outerAttrs = lazyRow.attrs.filter(a => a.readsOuter)
239
248
  const outerTexts = lazyRow.texts.filter(t => t.readsOuter)
240
- if (outerAttrs.length > 0 || outerTexts.length > 0) {
249
+ const outerConds = lazyRow.conditionals.filter(c => c.readsOuter)
250
+ if (outerAttrs.length > 0 || outerTexts.length > 0 || outerConds.length > 0) {
241
251
  const b3 = `${indent} `
242
252
  lines.push(`${b1}applyOuter: (__es, __seed) => {`)
243
253
  // Prime the outer reads so this ONE loop-level effect subscribes even
@@ -245,13 +255,20 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
245
255
  for (const g of lazyRow.outerPrimeGetters) lines.push(`${b2}${g}()`)
246
256
  lines.push(`${b2}for (const __e of __es) {`)
247
257
  lines.push(`${b3}const ${paramHead} = () => __e.item`)
248
- lines.push(`${b3}const __r = __e.refs ?? (__e.refs = ${claimVar}(__e))`)
258
+ lines.push(`${b3}const __r = __e.refs ?? (__e.refs = [])`)
249
259
  lines.push(`${b3}const __l = __e.last ?? (__e.last = [])`)
260
+ // Per ENTRY, not once for the batch: the preamble reads the item, so its
261
+ // locals differ per row. The outer signals it reads are already primed
262
+ // above, so this effect subscribes even when `__es` is empty.
263
+ if (lazyRow.outerNeedsPreamble && lazyRow.preambleStatements) {
264
+ lines.push(`${b3}${lazyRow.preambleStatements}`)
265
+ }
250
266
  for (const a of outerAttrs) emitAttrBinding(lines, b3, a, 'outer')
251
267
  if (outerTexts.length > 0) {
252
268
  lines.push(`${b3}const __d = ${doorAccess(lazyRow, lazyRow.writerIndex, adoptedPlanVar)}`)
253
269
  for (const t of outerTexts) emitTextBinding(lines, b3, t, '__d', 'outer', rwDoor)
254
270
  }
271
+ for (const c of outerConds) emitConditional(lines, b3, mid, c, 'outer')
255
272
  lines.push(`${b2}}`)
256
273
  lines.push(`${b1}},`)
257
274
  }
@@ -260,9 +277,10 @@ export function stringifyLazyRowLoop(lines: string[], o: StringifyLazyRowOptions
260
277
  }
261
278
 
262
279
  /**
263
- * The `entry.refs` array contents: one element ref per reactive-attr slot
264
- * (in `attrSlotIds` order), then — when the row has text slots — the
265
- * claimed-slot door at `writerIndex`.
280
+ * The `entry.refs` array `createRow` seeds: one element ref per reactive-attr
281
+ * slot (in `attrSlotIds` order), then — when the row has text slots — the
282
+ * claimed-slot door at `writerIndex`. Fresh-clone context only; an ADOPTED row
283
+ * starts empty and fills each slot on demand (`elementAccess` / `doorAccess`).
266
284
  *
267
285
  * **Which door (per LOOP, never per binding).** `lazySlots` returns a bare
268
286
  * write function; `lazyClaimSlots` returns the `{ write, read }` pair over
@@ -312,7 +330,6 @@ function refParts(
312
330
  elVar: string,
313
331
  skeletonPaths: SkeletonSlotPaths | null,
314
332
  planVar: string | null,
315
- deferDoor = false,
316
333
  ): string[] {
317
334
  const parts: string[] = []
318
335
  for (const slotId of lazyRow.attrSlotIds) {
@@ -320,11 +337,31 @@ function refParts(
320
337
  parts.push(path ? pathExpr(elVar, path) : `qsa(${elVar}, '[bf="${slotId}"]')`)
321
338
  }
322
339
  if (lazyRow.texts.length > 0) {
323
- parts.push(deferDoor ? 'null' : `${doorCtor(lazyRow)}(${elVar}, ${planVar})`)
340
+ parts.push(`${doorCtor(lazyRow)}(${elVar}, ${planVar})`)
324
341
  }
325
342
  return parts
326
343
  }
327
344
 
345
+ /**
346
+ * How an ADOPTED row's binding reaches its element ref: claim THIS slot, and
347
+ * only this slot, on first use.
348
+ *
349
+ * The previous shape claimed the whole row in one closure (`__lzc_<mid>`),
350
+ * which meant an `applyOuter` driving ONE attribute still ran a `qsa` scan for
351
+ * every OTHER reactive-attr slot in the row — on every row, at hydration. The
352
+ * `in` test (not `??`) is what makes the cache honest: `qsa` answers `null` for
353
+ * a slot it cannot find, and `null` is a real cached answer — the `in` test
354
+ * treats it as one and never scans again, where a `??`-guarded cache would
355
+ * re-scan that slot on every tick forever.
356
+ *
357
+ * This is the element-ref twin of `doorAccess`, which already deferred the
358
+ * content door for exactly the same reason.
359
+ */
360
+ function elementAccess(a: LazyRowAttrBinding): string {
361
+ const slot = `__r[${a.refIndex}]`
362
+ return `${a.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf="${a.slotId}"]'))`
363
+ }
364
+
328
365
  /** The door constructor for this loop — see `refParts`'s "which door" note. */
329
366
  function doorCtor(lazyRow: LazyRowPlanData): string {
330
367
  return lazyRow.textNeedsRead ? 'lazyClaimSlots' : 'lazySlots'
@@ -354,6 +391,63 @@ function doorAccess(
354
391
  return `${slot} ?? (${slot} = ${doorCtor(lazyRow)}(__e.primaryEl, ${adoptedPlanVar}))`
355
392
  }
356
393
 
394
+ /** Hoisted arm-template variable names for one conditional of one loop. */
395
+ function condVars(mid: string, slotId: string): { trueTpl: string; falseTpl: string } {
396
+ const key = `${mid}_${slotId.replace(/[^A-Za-z0-9_$]/g, '_')}`
397
+ return { trueTpl: `__cbt_${key}`, falseTpl: `__cbf_${key}` }
398
+ }
399
+
400
+ /**
401
+ * A row conditional, in the same three modes as {@link emitAttrBinding}.
402
+ *
403
+ * The whole swap is `replaceWith` on the `[bf-c]` element, because
404
+ * `analyzeLazyConditional` has already established that the arms own nothing —
405
+ * no events, no children to init, no live nodes to splice. What `insert()` does
406
+ * per row with an effect of its own, this does with a boolean and a dedup slot.
407
+ *
408
+ * **`'create'` writes no DOM.** The row `createRow` just cloned came from the
409
+ * per-row template, which interpolated `cond ? trueHtml : falseHtml` itself — so
410
+ * the correct arm is already in place and the only thing missing is the dedup
411
+ * record. Swapping here would replace an element with an identical one.
412
+ *
413
+ * **`'outer'` seeds by read-compare-write (§9.3(1))** against
414
+ * `content.firstElementChild.outerHTML` — the browser's serialization of the arm
415
+ * we would install, compared with the serialization of what is there. Both sides
416
+ * go through the DOM, so a match means the server already rendered this arm and
417
+ * no write happens.
418
+ *
419
+ * The ref is REASSIGNED after a swap (`__r[i] = __n`). Without that, the next
420
+ * apply would hold the detached node and write into nothing.
421
+ */
422
+ function emitConditional(
423
+ lines: string[],
424
+ ind: string,
425
+ mid: string,
426
+ c: LazyRowConditionalBinding,
427
+ mode: 'create' | 'item' | 'outer',
428
+ ): void {
429
+ const v = condVars(mid, c.slotId)
430
+ if (mode === 'create') {
431
+ lines.push(`${ind}__l[${c.ordinal}] = !!(${c.wrappedCondition})`)
432
+ return
433
+ }
434
+ const slot = `__r[${c.refIndex}]`
435
+ lines.push(`${ind}{ const __c = ${c.refIndex} in __r ? ${slot} : (${slot} = qsa(__e.primaryEl, '[bf-c="${c.slotId}"]'))`)
436
+ lines.push(`${ind}if (__c) {`)
437
+ lines.push(`${ind} const __x = !!(${c.wrappedCondition})`)
438
+ lines.push(`${ind} const __w = (__x ? ${v.trueTpl} : ${v.falseTpl}).content.firstElementChild`)
439
+ const guard = mode === 'item'
440
+ ? dedupGuard(c.ordinal)
441
+ : `__seed ? (__c.outerHTML !== __w.outerHTML) : (${dedupGuard(c.ordinal)})`
442
+ lines.push(`${ind} if (${guard}) {`)
443
+ lines.push(`${ind} const __n = __w.cloneNode(true)`)
444
+ lines.push(`${ind} __c.replaceWith(__n)`)
445
+ lines.push(`${ind} ${slot} = __n`)
446
+ lines.push(`${ind} }`)
447
+ lines.push(`${ind} __l[${c.ordinal}] = __x`)
448
+ lines.push(`${ind}} }`)
449
+ }
450
+
357
451
  /** `entry.last`-backed dedup test. `in` (not a truthiness check) so a
358
452
  * legitimately `undefined` value still records and still writes once. */
359
453
  function dedupGuard(ordinal: number): string {
@@ -366,7 +460,11 @@ function emitAttrBinding(
366
460
  a: LazyRowAttrBinding,
367
461
  mode: 'create' | 'item' | 'outer',
368
462
  ): void {
369
- lines.push(`${ind}{ const __t = __r[${a.refIndex}]`)
463
+ // `createRow` seeded every ref from a known clone path, so there it is a
464
+ // plain read. An ADOPTED row starts with an EMPTY `refs` array and each
465
+ // binding claims its OWN slot on first use — see `elementAccess`.
466
+ const target = mode === 'create' ? `__r[${a.refIndex}]` : elementAccess(a)
467
+ lines.push(`${ind}{ const __t = ${target}`)
370
468
  lines.push(`${ind}if (__t) {`)
371
469
  lines.push(`${ind} const __x = ${a.wrappedExpression}`)
372
470
  const guard = mode === 'create'
@@ -124,7 +124,7 @@ export function stringifyReactiveEffects(
124
124
  if (pc) {
125
125
  // Profile mode: preserve the granular per-slot/per-attr effect shape so
126
126
  // every binding keeps its own bfId (see module docstring).
127
- emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId)
127
+ emitAttrSlotsGranular(lines, indent, elVar, lookup, attrSlots, elementIndexBySlot, bindingBfId, mapPreambleWrapped)
128
128
  emitOuterTexts(lines, indent, elVar, outerTexts, bindingBfId, textClaimPathExprs)
129
129
  emitPreambleRegionsEffect(lines, indent, elVar, preambleRegions, mapPreambleWrapped)
130
130
  } else {
@@ -154,6 +154,7 @@ function emitAttrSlotsGranular(
154
154
  attrSlots: readonly ReactiveAttrSlot[],
155
155
  elementIndexBySlot: ReadonlyMap<string, number> | undefined,
156
156
  bindingBfId: (slotId: string) => string,
157
+ mapPreambleWrapped: string | undefined,
157
158
  ): void {
158
159
  for (const slot of attrSlots) {
159
160
  const varName = `__ra_${varSlotId(slot.slotId)}`
@@ -162,6 +163,14 @@ function emitAttrSlotsGranular(
162
163
  lines.push(`${indent}if (${varName}) {`)
163
164
  for (const attr of slot.attrs) {
164
165
  lines.push(`${indent} createEffect(() => {`)
166
+ // Profile mode keeps one effect per attr, so an attr reading a preamble
167
+ // local needs the declarations inside ITS OWN effect — there is no
168
+ // shared row-effect scope to hoist them into here (#2447 follow-up).
169
+ // Re-running per attr is the price of the per-binding bfId this mode
170
+ // exists to produce; `bf debug profile` is off the build hot path.
171
+ if (attr.readsPreamble && mapPreambleWrapped) {
172
+ lines.push(`${indent} ${mapPreambleWrapped}`)
173
+ }
165
174
  for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
166
175
  lines.push(`${indent} ${stmt}`)
167
176
  }
@@ -193,6 +202,11 @@ function emitPreambleRegionsEffect(
193
202
  lines.push(`${indent}})`)
194
203
  }
195
204
 
205
+ /** Does any attr in these slots read a `.map()` preamble local (#2447)? */
206
+ function attrsReadPreamble(attrSlots: readonly ReactiveAttrSlot[]): boolean {
207
+ return attrSlots.some(slot => slot.attrs.some(a => a.readsPreamble))
208
+ }
209
+
196
210
  function attrLookupExpr(
197
211
  slotId: string,
198
212
  varName: string,
@@ -270,6 +284,15 @@ function emitConsolidatedRowEffect(
270
284
  return
271
285
  }
272
286
  lines.push(`${indent}createEffect(() => {`)
287
+ // The preamble re-run leads the body, not trails it (#2447 follow-up).
288
+ // It used to sit between the attr writes and the region writes, because
289
+ // regions were its only readers. An attribute can read a preamble local
290
+ // now (`class={cls}`), and it reads it from THIS scope — so the
291
+ // declarations have to exist before the first write, whichever kind that
292
+ // is. Regions are unaffected by the move: they were already downstream.
293
+ if (mapPreambleWrapped && (preambleRegions.length > 0 || attrsReadPreamble(attrSlots))) {
294
+ lines.push(`${indent} ${mapPreambleWrapped}`)
295
+ }
273
296
  for (const slot of attrSlots) {
274
297
  const varName = `__ra_${varSlotId(slot.slotId)}`
275
298
  lines.push(`${indent} if (${varName}) {`)
@@ -282,9 +305,6 @@ function emitConsolidatedRowEffect(
282
305
  }
283
306
  lines.push(`${indent} }`)
284
307
  }
285
- if (preambleRegions.length > 0 && mapPreambleWrapped) {
286
- lines.push(`${indent} ${mapPreambleWrapped}`)
287
- }
288
308
  for (const text of outerTexts) {
289
309
  lines.push(`${indent} ${writer}('${text.slotId}', String(${text.wrappedExpression}))`)
290
310
  }
@@ -447,7 +447,12 @@ export function buildSignalMemoEnv(
447
447
  for (const m of memos) {
448
448
  substitutions.set(m.name, {
449
449
  kind: 'call',
450
- replacement: extractMemoBodyExpr(m.computation),
450
+ // Destructured mode (#2468): `templateComputation` (when present) has
451
+ // bare destructured prop refs already rewritten to `_p.X` — the
452
+ // spliced body lands in the module-scope template arrow, which is not
453
+ // a closure over init's `const value = _p.value` extraction. Mirrors
454
+ // the signal `templateInitialValue` handling above (#2265).
455
+ replacement: extractMemoBodyExpr(m.templateComputation ?? m.computation),
451
456
  freeIdentifiers: m.computationFreeIdentifiers ?? new Set(),
452
457
  })
453
458
  }
@@ -170,7 +170,7 @@ export function emitRegistrationAndHydration(
170
170
  // transformation runs at this layer (#1277).
171
171
  const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
172
172
  const templateHtml = generateCsrTemplate(
173
- _ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
173
+ _ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames, ctx.deferredChildSlots
174
174
  )
175
175
  if (templateHtml) {
176
176
  defParts.push(`template: (${PROPS_PARAM}) => \`${templateHtml}\``)
@@ -12,6 +12,7 @@ import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } fro
12
12
  import type { ClientJsContext } from './types.ts'
13
13
  import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
14
14
  import { buildLoopChainExpr } from '../loop-chain.ts'
15
+ import { derivesScopeFromSlot } from '../adapters/child-scope.ts'
15
16
 
16
17
  /**
17
18
  * Protect string literals from regex-based replacements.
@@ -547,12 +548,11 @@ function buildSpreadAttrsMergeCall(args: {
547
548
  * are wrapped with `__bfSlot(EXPR, <branchSlotsVar>)` so the runtime can
548
549
  * splice live `Node` returns into the parsed fragment instead of
549
550
  * stringifying them via the template literal (#1213).
550
- * @param insideLoop - When true, component nodes omit their `slotId` from the
551
- * `renderChild` call. Each loop iteration produces its own scope (identified
552
- * by `data-key`), so the parent-slot suffix would create scopes that don't
553
- * match the SSR shape see #1268 and the matching `insideLoop` guard in
554
- * `generateCsrTemplate` (case `'component'`). Set to `true` when generating
555
- * the per-iteration `staticItemTemplate` for static loops.
551
+ * Component nodes omit their `slotId` from the `renderChild` call exactly
552
+ * when `derivesScopeFromSlot()` says so a loop-item-root component (its
553
+ * own scope, identified by `data-key`) never derives from the parent slot;
554
+ * a component nested below the row root does (#2444). See
555
+ * `IRComponent.loopItemRoot` / `./adapters/child-scope.ts`.
556
556
  */
557
557
  /**
558
558
  * Build the per-item `<!--bf-loop-i:KEY-->` anchor comment for a whole-item
@@ -660,7 +660,7 @@ export function renderFlatMapClientBody(
660
660
  rawLeaf: true,
661
661
  renderLeaf: (ir) => {
662
662
  const key = flatMapLeafKeyExpr(ir)
663
- const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined, true)
663
+ const html = irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, 1, undefined, undefined)
664
664
  return `({ k: ${key ?? 'undefined'}, h: \`${html}\` })`
665
665
  },
666
666
  })
@@ -691,7 +691,7 @@ export function renderFlatMapProjectionClientBody(
691
691
  const params = inner.index ? `(${inner.param}, ${inner.index})` : `(${inner.param})`
692
692
  const key = inner.key ? `(${inner.key})` : 'undefined'
693
693
  const html = inner.children
694
- .map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined, true))
694
+ .map((c) => irToHtmlTemplate(escapeLeafTextExpressions(c), restSpreadNames, 1, undefined, undefined))
695
695
  .join('')
696
696
  return `${chained}.map(${params} => ({ k: ${key}, h: \`${html}\` }))`
697
697
  }
@@ -728,8 +728,8 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
728
728
  }
729
729
  }
730
730
 
731
- export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, insideLoop = false, inHoistedChildren = false): string {
732
- const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, inHoistedChildren)
731
+ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
732
+ const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
733
733
  const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
734
734
  const wrapInterpolation = (expr: string): string => branchSlotsVar
735
735
  ? `__bfSlot(${expr}, ${branchSlotsVar})`
@@ -781,7 +781,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
781
781
  }
782
782
 
783
783
  const attrs = attrParts.join(' ')
784
- const childrenRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, false)
784
+ const childrenRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, false)
785
785
  const children = dangerouslyHtmlChildren(node.attrs, v => wrapExpr(v.expr)) ?? node.children.map(childrenRecurse).join('')
786
786
 
787
787
  // Non-void elements must use open+close tags (HTML parsers ignore self-closing on div, span, etc.)
@@ -853,13 +853,17 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
853
853
  // instead of emitting empty placeholders (#435).
854
854
  const propsEntries = node.props
855
855
  .filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
856
- .filter(p => !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
856
+ // `ref` joins the `on*` handlers here: a function prop cannot run
857
+ // during module-scope string rendering, and a ref closing over an
858
+ // init-scope setter would leak it into the template (#2468). The
859
+ // init path re-applies refs once real DOM exists.
860
+ .filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
857
861
  .map(p => {
858
862
  // `/* @client */` defers the prop to hydrate via initChild.
859
863
  if (p.clientOnly) return null
860
864
  switch (p.value.kind) {
861
865
  case 'jsx-children': {
862
- const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, insideLoop, true)
866
+ const hoistedRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, true)
863
867
  const childHtml = p.value.children.map(c => hoistedRecurse(c)).join('')
864
868
  return `${quotePropName(p.name)}: \`${childHtml}\``
865
869
  }
@@ -872,7 +876,15 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
872
876
  case 'expression':
873
877
  case 'template':
874
878
  case 'spread': {
875
- const expr = attrValueToString(p.value) ?? 'undefined'
879
+ // Template-variant form (`useTemplate`): this props object
880
+ // lands inside the module-scope registration template, which
881
+ // is not a closure over init's destructured-prop extractions
882
+ // — the raw form leaked bare refs like `${className}` into
883
+ // `renderChild(...)` (#2468). Mirrors the sibling component
884
+ // emit sites (`irToComponentTemplateWithOpts`,
885
+ // `generateCsrTemplateWithOpts`), which already pick the
886
+ // prop-rewritten variant.
887
+ const expr = attrValueToString(p.value, { useTemplate: true }) ?? 'undefined'
876
888
  return `${quotePropName(p.name)}: ${expr}`
877
889
  }
878
890
  }
@@ -884,11 +896,12 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
884
896
  const keyProp = node.props.find(p => p.name === 'key')
885
897
  const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? 'undefined'}` : ''
886
898
  // Pass slotId as suffix so $c() can find the child component by slot
887
- // after branch swap. Inside a loop body each iteration owns a
888
- // separate scope (identified by `data-key`), so the parent-slot
889
- // suffix is dropped to avoid anchoring a deeply-nested child to a
890
- // wrong component-wrapper scope (#1268).
891
- const slotArg = (!insideLoop && node.slotId) ? `, '${node.slotId}'` : ''
899
+ // after branch swap. A loop-item-root component owns a separate scope
900
+ // per iteration (identified by `data-key`), so the parent-slot suffix
901
+ // is dropped to avoid anchoring it to a wrong component-wrapper scope
902
+ // (#1268) — a component nested BELOW the row root still derives from
903
+ // its slot, matching the Hono reference (#2444).
904
+ const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
892
905
  return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
893
906
  }
894
907
 
@@ -897,13 +910,7 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
897
910
  // Increment loopDepth so inner key attrs become data-key-N
898
911
  // Forward loopParams so expressions referencing outer/inner loop params
899
912
  // get wrapped as signal accessors (e.g., task.title → task().title).
900
- // `insideLoop` is preserved (not forced to `true`): downstream branch /
901
- // inner-loop templates depend on the legacy slot-suffix-keeping shape
902
- // to find scopes via `findSsrScopeBySlotIn`. The opt-in at the entry
903
- // call site is the only place that wants the suffix dropped (#1268
904
- // Case 1 — childComponent body materialize); propagating it through
905
- // every nested loop regressed form-builder's inner-loop Select wiring.
906
- const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop)
913
+ const innerRecurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar)
907
914
  let childTemplate = node.children.map(innerRecurse).join('')
908
915
  // Whole-item conditional loops (#1665): prepend an always-present
909
916
  // `<!--bf-loop-i:KEY-->` anchor before each item's (possibly empty)
@@ -938,13 +945,13 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
938
945
  // identity is stamped by mapArray via setAttribute — emitting it
939
946
  // here was the CSR/SSR data-key asymmetry (unescaped, client-only).
940
947
  const body = renderPreamble(node.flatMapCallback, {
941
- renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop),
948
+ renderLeaf: (ir) => irToHtmlTemplate(stripLeafKeyAttr(ir), restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar),
942
949
  })
943
950
  mapExpr = `\${${wrappedArray}.flatMap(${node.flatMapCallback.params} => ${body}).join('')}`
944
951
  } else if (node.preamble) {
945
952
  // Stage 3 / D4 — render JSX leaves in an arbitrary array-builder
946
953
  // preamble (the hydrate-template context uses the bare loop param).
947
- const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar, insideLoop) })
954
+ const preamble = renderPreamble(node.preamble, { textVariant: 'client', renderLeaf: (ir) => irToHtmlTemplate(ir, restSpreadNames, loopDepth + 1, loopParams, branchSlotsVar) })
948
955
  mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
949
956
  } else {
950
957
  mapExpr = `\${${wrappedArray}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
@@ -1563,7 +1570,6 @@ export interface TemplateOptions {
1563
1570
  * `compute-inlinability` (#1277).
1564
1571
  */
1565
1572
  csrEnv?: CsrEnv
1566
- insideLoop?: boolean
1567
1573
  loopDepth?: number
1568
1574
  /**
1569
1575
  * Names bound by the ENCLOSING loops' callback params (item / index /
@@ -1799,7 +1805,11 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
1799
1805
  // Use renderChild() to render child component's template at runtime (#435)
1800
1806
  const propsEntries = node.props
1801
1807
  .filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
1802
- .filter(p => !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
1808
+ // `ref` joins the `on*` handlers here: a function prop cannot run
1809
+ // during module-scope string rendering, and a ref closing over an
1810
+ // init-scope setter would leak it into the template (#2468). The
1811
+ // init path re-applies refs once real DOM exists.
1812
+ .filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
1803
1813
  .map(p => {
1804
1814
  // `/* @client */` defers the prop to hydrate via initChild.
1805
1815
  if (p.clientOnly) return null
@@ -1990,7 +2000,6 @@ export function generateCsrTemplate(
1990
2000
  node: IRNode,
1991
2001
  inlinableConstants: Map<string, string> | undefined,
1992
2002
  ctx: ClientJsContext,
1993
- insideLoop?: boolean,
1994
2003
  restSpreadNames?: Set<string>,
1995
2004
  propsObjectName?: string | null,
1996
2005
  unsafeLocalNames?: Set<string>,
@@ -2010,7 +2019,7 @@ export function generateCsrTemplate(
2010
2019
  }
2011
2020
  }
2012
2021
  const effectiveUnsafeLocalNames = mergeCsrNullUnsafe(ctx, unsafeLocalNames)
2013
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 })
2022
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1 })
2014
2023
  }
2015
2024
 
2016
2025
  /**
@@ -2171,6 +2180,7 @@ export function computeDeferredChildSlots(
2171
2180
  // filter set MUST mirror the `propsEntries` filter in the CSR
2172
2181
  // `component` emit below so the deferral decision matches output.
2173
2182
  if (p.name === '...' || p.name.startsWith('...') || p.name === 'key') return false
2183
+ if (p.name === 'ref') return false
2174
2184
  if (p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()) return false
2175
2185
  if (p.clientOnly) return false
2176
2186
  return propResolvesUnsafe(p, env, unsafeLocalNames)
@@ -2204,7 +2214,7 @@ export function computeDeferredChildSlots(
2204
2214
  }
2205
2215
 
2206
2216
  function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
2207
- const { restSpreadNames, propsObjectName, csrEnv, insideLoop, unsafeLocalNames, loopDepth = 0 } = opts
2217
+ const { restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
2208
2218
  const env: CsrEnv = csrEnv ?? { substitutions: new Map(), propsObjectName: propsObjectName ?? null }
2209
2219
  const transformExpr = (expr: string, templateExpr?: string): string => {
2210
2220
  // Single AST substitution pass: replaces signal getter calls
@@ -2241,12 +2251,10 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2241
2251
  }
2242
2252
 
2243
2253
  // `recurse` preserves `inHoistedChildren` for structural pass-through
2244
- // (fragment / conditional); `childrenRecurse` and `recurseInLoop`
2245
- // clear it — a new element root or loop iteration starts a fresh
2246
- // scope. (#1320)
2254
+ // (fragment / conditional); `childrenRecurse` clears it — a new element
2255
+ // root starts a fresh scope. (#1320)
2247
2256
  const recurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, opts)
2248
2257
  const childrenRecurse = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, inHoistedChildren: false })
2249
- const recurseInLoop = (n: IRNode): string => generateCsrTemplateWithOpts(n, { ...opts, insideLoop: true, loopDepth: loopDepth + 1, inHoistedChildren: false })
2250
2258
 
2251
2259
  switch (node.type) {
2252
2260
  case 'element': {
@@ -2399,7 +2407,11 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2399
2407
 
2400
2408
  const propsEntries = node.props
2401
2409
  .filter(p => p.name !== '...' && !p.name.startsWith('...') && p.name !== 'key')
2402
- .filter(p => !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
2410
+ // `ref` joins the `on*` handlers here: a function prop cannot run
2411
+ // during module-scope string rendering, and a ref closing over an
2412
+ // init-scope setter would leak it into the template (#2468). The
2413
+ // init path re-applies refs once real DOM exists.
2414
+ .filter(p => p.name !== 'ref' && !(p.name.startsWith('on') && p.name.length > 2 && p.name[2] === p.name[2].toUpperCase()))
2403
2415
  .map(p => {
2404
2416
  // `/* @client */` defers the prop to hydrate. Drop from the
2405
2417
  // SSR `renderChild` props — `initChild`'s `propsExpr` getter
@@ -2441,7 +2453,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2441
2453
  const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(', ')}}` : '{}'
2442
2454
  const keyProp = node.props.find(p => p.name === 'key')
2443
2455
  const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : ''
2444
- const slotArg = (!insideLoop && node.slotId) ? `, '${node.slotId}'` : ''
2456
+ const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
2445
2457
  return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
2446
2458
  }
2447
2459
 
@@ -2469,7 +2481,6 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2469
2481
  }
2470
2482
  const recurseInLoopBody = (n: IRNode): string => generateCsrTemplateWithOpts(n, {
2471
2483
  ...opts,
2472
- insideLoop: true,
2473
2484
  loopDepth: loopDepth + 1,
2474
2485
  inHoistedChildren: false,
2475
2486
  loopBoundNames: boundHere,
@@ -267,7 +267,7 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
267
267
  if (!templateHtml) {
268
268
  const csrInlinableConstants = csrInlinableConstantsFromCtx(ctx)
269
269
  templateHtml = generateCsrTemplate(
270
- ir.root, csrInlinableConstants, ctx, undefined, restSpreadNames, ctx.propsObjectName, unsafeLocalNames
270
+ ir.root, csrInlinableConstants, ctx, restSpreadNames, ctx.propsObjectName, unsafeLocalNames
271
271
  )
272
272
  }
273
273
 
@@ -95,6 +95,7 @@ function buildSignalPlan(
95
95
  kind: 'signal',
96
96
  getter: signal.getter,
97
97
  setter: signal.setter,
98
+ getterElided: signal.getterElided,
98
99
  initialValueExpr: '',
99
100
  controlledEffect: null,
100
101
  initializerOverride: `${factory}()`,
@@ -109,6 +110,7 @@ function buildSignalPlan(
109
110
  kind: 'signal',
110
111
  getter: signal.getter,
111
112
  setter: signal.setter,
113
+ getterElided: signal.getterElided,
112
114
  initialValueExpr: resolveSignalInitialValue(signal, ctx, lookups),
113
115
  controlledEffect,
114
116
  branchCondition: signal.branchCondition,