@barefootjs/jsx 0.35.0 → 0.35.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 (72) hide show
  1. package/dist/analyzer-context.d.ts.map +1 -1
  2. package/dist/index.js +731 -691
  3. package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
  4. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  5. package/dist/ir-to-client-js/control-flow/plan/build-composite-loop.d.ts.map +1 -1
  6. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts +14 -2
  7. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
  9. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +3 -2
  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/inner-loop.d.ts +24 -44
  15. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
  17. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
  19. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
  21. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
  23. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
  26. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
  28. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/reactivity.d.ts +62 -18
  31. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/types.d.ts +18 -2
  33. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  34. package/dist/reactivity-checker.d.ts +18 -0
  35. package/dist/reactivity-checker.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +128 -10
  38. package/src/__tests__/binding-scope-ratchet.test.ts +26 -3
  39. package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
  40. package/src/__tests__/compiler-stress-1244.test.ts +8 -8
  41. package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
  42. package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
  43. package/src/__tests__/loop-index-reactivity.test.ts +208 -0
  44. package/src/__tests__/map-body-brand-checker.test.ts +176 -0
  45. package/src/__tests__/nested-loop-index-param.test.ts +27 -5
  46. package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
  47. package/src/__tests__/nested-loop-plain.test.ts +39 -25
  48. package/src/__tests__/nested-loop-reactive-attrs.test.ts +12 -4
  49. package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
  50. package/src/__tests__/style-css-var-reactive.test.ts +4 -1
  51. package/src/analyzer-context.ts +12 -10
  52. package/src/ir-to-client-js/collect-elements.ts +110 -32
  53. package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -0
  54. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +28 -51
  55. package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +7 -2
  56. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +33 -6
  57. package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +7 -1
  58. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +18 -6
  59. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
  60. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
  61. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
  62. package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
  63. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +12 -15
  64. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
  65. package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
  66. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
  67. package/src/ir-to-client-js/emit-reactive.ts +88 -5
  68. package/src/ir-to-client-js/html-template.ts +9 -1
  69. package/src/ir-to-client-js/reactivity.ts +73 -27
  70. package/src/ir-to-client-js/types.ts +19 -3
  71. package/src/jsx-to-ir.ts +21 -4
  72. package/src/reactivity-checker.ts +60 -0
@@ -218,9 +218,14 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
218
218
  const condFacts: LazyConditionalFacts[] = []
219
219
  let conditionalRefusal: string | null = null
220
220
  for (const cond of rawConditionals) {
221
+ // whenTrueHtml/whenFalseHtml are already loop-param-wrapped at IR
222
+ // render time (`irToHtmlTemplate`'s `loopParams`, collect-elements.ts)
223
+ // — do NOT re-wrap the rendered HTML string here (#2868: a
224
+ // word-boundary regex over assembled markup can match a bare tag name
225
+ // colliding with the param/index identifier, e.g. `<i>` -> `<i()>`).
221
226
  const verdict = analyzeLazyConditional(cond, {
222
- whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
223
- whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
227
+ whenTrueHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
228
+ whenFalseHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
224
229
  })
225
230
  if (!verdict.lazySafe) { conditionalRefusal = verdict.reason; break }
226
231
  condFacts.push(verdict.facts)
@@ -202,6 +202,8 @@ export interface BuildBranchInnerLoopsArgs {
202
202
  outerLoopParam: string
203
203
  /** Outer loop param destructuring metadata. */
204
204
  outerLoopParamBindings?: readonly LoopParamBinding[]
205
+ /** Outer loop's index param name, when present (#2861). */
206
+ outerLoopIndex?: string | null
205
207
  /**
206
208
  * Outer-wrap closure — defaults to wrapping with `outerLoopParam`. Overridden
207
209
  * by `emitNestedLoopChildConditionals` recursion (which threads its own wrap).
@@ -225,6 +227,7 @@ export function buildBranchInnerLoopsPlan(
225
227
  condSlotId,
226
228
  outerLoopParam,
227
229
  outerLoopParamBindings,
230
+ outerLoopIndex,
228
231
  wrapOuter,
229
232
  } = args
230
233
  if (!innerLoops || innerLoops.length === 0) return []
@@ -232,7 +235,12 @@ export function buildBranchInnerLoopsPlan(
232
235
  const plan: BranchInnerLoop[] = []
233
236
  for (let i = 0; i < innerLoops.length; i++) {
234
237
  const inner = innerLoops[i]
235
- if (!inner.refsOuterParam || !inner.template) continue
238
+ // #2865: an inner loop inside a conditional arm used to be dropped
239
+ // entirely (no `mapArray`, no hydration wiring at all) whenever its
240
+ // array didn't reference the outer loop's item — the same unsound
241
+ // gate `build-inner-loop.ts` used, just answered by skipping instead
242
+ // of falling back to a weaker static emission. Always build the plan.
243
+ if (!inner.template) continue
236
244
 
237
245
  const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings, inner.index)
238
246
  const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings, inner.index)
@@ -327,11 +335,13 @@ export function buildBranchInnerLoopsPlan(
327
335
  wrap: wrapBoth,
328
336
  loopParam: inner.param,
329
337
  loopParamBindings: inner.paramBindings,
338
+ loopIndex: inner.index,
330
339
  }),
331
340
  innerLoopParam: inner.param,
332
341
  innerLoopParamBindings: inner.paramBindings,
333
342
  outerLoopParam,
334
343
  outerLoopParamBindings,
344
+ outerLoopIndex,
335
345
  })
336
346
  }
337
347
  return plan
@@ -352,6 +362,8 @@ export interface BuildLoopChildConditionalsArgs {
352
362
  loopParam: string
353
363
  /** Loop param destructuring metadata. */
354
364
  loopParamBindings?: readonly LoopParamBinding[]
365
+ /** Loop's index param name, when it declares one (#2861) — same role as `loopParam` above. */
366
+ loopIndex?: string | null
355
367
  }
356
368
 
357
369
  /**
@@ -360,7 +372,8 @@ export interface BuildLoopChildConditionalsArgs {
360
372
  * recursion. Each conditional pre-builds:
361
373
  *
362
374
  * - the wrapped condition expression
363
- * - the wrapped + addCondAttr'd whenTrue / whenFalse template HTML
375
+ * - the addCondAttr'd whenTrue / whenFalse template HTML (already
376
+ * loop-param-wrapped at IR render time, #2868)
364
377
  * - per-arm `LoopChildArmPlan`: events / child components / inner
365
378
  * loops / nested conditionals (recursion)
366
379
  *
@@ -370,7 +383,7 @@ export interface BuildLoopChildConditionalsArgs {
370
383
  export function buildLoopChildConditionalsPlan(
371
384
  args: BuildLoopChildConditionalsArgs,
372
385
  ): LoopChildConditionalPlan[] {
373
- const { conditionals, scopeVar, wrap, loopParam, loopParamBindings } = args
386
+ const { conditionals, scopeVar, wrap, loopParam, loopParamBindings, loopIndex } = args
374
387
  if (!conditionals || conditionals.length === 0) return []
375
388
 
376
389
  const plans: LoopChildConditionalPlan[] = []
@@ -379,13 +392,20 @@ export function buildLoopChildConditionalsPlan(
379
392
  slotId: cond.slotId,
380
393
  scopeVar,
381
394
  wrappedCondition: wrap(cond.condition),
382
- whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
383
- whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
395
+ // whenTrueHtml/whenFalseHtml are already loop-param-wrapped for the
396
+ // full ancestor chain at IR render time (`irToHtmlTemplate`'s
397
+ // `loopParams`, collect-elements.ts) — do NOT re-wrap the rendered
398
+ // HTML string here (#2868: a word-boundary regex over assembled
399
+ // markup can match a bare tag name colliding with the param/index
400
+ // identifier, e.g. `<i>` -> `<i()>`).
401
+ whenTrueTemplateHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
402
+ whenFalseTemplateHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
384
403
  whenTrueArm: buildLoopChildArmPlan({
385
404
  branch: cond.whenTrue,
386
405
  wrap,
387
406
  loopParam,
388
407
  loopParamBindings,
408
+ loopIndex,
389
409
  condId: cond.slotId,
390
410
  }),
391
411
  whenFalseArm: buildLoopChildArmPlan({
@@ -393,8 +413,10 @@ export function buildLoopChildConditionalsPlan(
393
413
  wrap,
394
414
  loopParam,
395
415
  loopParamBindings,
416
+ loopIndex,
396
417
  condId: cond.slotId,
397
418
  }),
419
+ ...(cond.readsPreamble && { readsPreamble: true }),
398
420
  })
399
421
  }
400
422
  return plans
@@ -429,6 +451,7 @@ export function buildArmAttrsPlan(
429
451
  attrName: attr.attrName,
430
452
  wrappedExpression: wrap(attr.expression),
431
453
  meta: pickAttrMeta(attr),
454
+ ...(attr.readsPreamble && { readsPreamble: true }),
432
455
  })),
433
456
  })
434
457
  }
@@ -455,12 +478,14 @@ interface BuildLoopChildArmArgs {
455
478
  wrap: (expr: string) => string
456
479
  loopParam: string
457
480
  loopParamBindings?: readonly LoopParamBinding[]
481
+ /** Loop's index param name, when it declares one (#2861). */
482
+ loopIndex?: string | null
458
483
  /** The enclosing conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
459
484
  condId: string
460
485
  }
461
486
 
462
487
  function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
463
- const { branch, wrap, loopParam, loopParamBindings, condId } = args
488
+ const { branch, wrap, loopParam, loopParamBindings, loopIndex, condId } = args
464
489
  return {
465
490
  events: buildBranchEventBindingsPlan({
466
491
  events: branch.events,
@@ -476,6 +501,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
476
501
  condSlotId: condId,
477
502
  outerLoopParam: loopParam,
478
503
  outerLoopParamBindings: loopParamBindings,
504
+ outerLoopIndex: loopIndex,
479
505
  wrapOuter: wrap,
480
506
  }),
481
507
  nestedConditionals: buildLoopChildConditionalsPlan({
@@ -484,6 +510,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
484
510
  wrap,
485
511
  loopParam,
486
512
  loopParamBindings,
513
+ loopIndex,
487
514
  }),
488
515
  attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
489
516
  texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
@@ -93,8 +93,14 @@ export function buildPlainRowCore(inputs: PlainRowInputs): PlainRowCore {
93
93
  const mapPreambleWrappedFinal = !lazyRow && loop.index
94
94
  ? wrapIndexParamAsAccessor(mapPreambleWrapped, loop.index)
95
95
  : mapPreambleWrapped
96
+ // `loop.templateIndexed` is a second structured render (index wrapped at
97
+ // IR time, `collect-elements.ts`) rather than a post-hoc regex pass over
98
+ // `loop.template` — a word-boundary regex over assembled HTML can match a
99
+ // bare tag name colliding with the index identifier (`<i>` -> `<i()>`,
100
+ // #2868). Falls back to `loop.template` for the rare shape that doesn't
101
+ // populate it (e.g. a flatMap projection loop).
96
102
  const templateFinal = !lazyRow && loop.index
97
- ? wrapIndexParamAsAccessor(loop.template, loop.index)
103
+ ? (loop.templateIndexed ?? loop.template)
98
104
  : loop.template
99
105
 
100
106
  return {
@@ -12,8 +12,9 @@
12
12
  * from `LoopChildBranchSummary.reactiveAttrs` / `.reactiveTexts` —
13
13
  * collected per-branch, recursively, so a doubly-nested conditional's
14
14
  * arm carries its own bindings instead of an outer scope reaching in.
15
- * 4. Apply `addCondAttrToTemplate` to the wrapped branch HTML so the
16
- * stringifier emits a ready-to-interpolate template literal.
15
+ * 4. Apply `addCondAttrToTemplate` to each branch's HTML (already
16
+ * loop-param-wrapped at IR render time, #2868) so the stringifier
17
+ * emits a ready-to-interpolate template literal.
17
18
  * 5. Recurse into the per-arm sub-plans via `LoopChildArmPlan` —
18
19
  * events, child component inits, inner loops, nested conditionals,
19
20
  * attrs, texts. No legacy passthrough remains.
@@ -110,10 +111,17 @@ export function buildReactiveEffectsPlan(
110
111
  conditionalPlans.push({
111
112
  slotId: cond.slotId,
112
113
  wrappedCondition: wrap(cond.condition),
113
- whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
114
- whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
115
- whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
116
- whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
114
+ // whenTrueHtml/whenFalseHtml are already loop-param-wrapped at IR
115
+ // render time (`irToHtmlTemplate`'s `loopParams`, collect-elements.ts)
116
+ // do NOT re-wrap the rendered HTML string here. A word-boundary
117
+ // regex over already-assembled markup matches a bare tag name that
118
+ // collides with the param/index identifier (`<i>` -> `<i()>`) just
119
+ // as readily as a real reference, since `<`/`>` are non-word
120
+ // characters (#2868).
121
+ whenTrueTemplateHtml: addCondAttrToTemplate(cond.whenTrueHtml, cond.slotId),
122
+ whenFalseTemplateHtml: addCondAttrToTemplate(cond.whenFalseHtml, cond.slotId),
123
+ whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, loopIndex, cond.slotId, profileComponentName),
124
+ whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, loopIndex, cond.slotId, profileComponentName),
117
125
  ...(cond.readsPreamble && { readsPreamble: true }),
118
126
  })
119
127
  }
@@ -132,6 +140,8 @@ function buildOuterArm(
132
140
  wrap: (expr: string) => string,
133
141
  loopParam: string,
134
142
  loopParamBindings: readonly LoopParamBinding[] | undefined,
143
+ /** This loop's index param name, when it declares one (#2861). */
144
+ loopIndex: string | null | undefined,
135
145
  /** The conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
136
146
  condSlotId: string,
137
147
  profileComponentName?: string,
@@ -152,6 +162,7 @@ function buildOuterArm(
152
162
  condSlotId,
153
163
  outerLoopParam: loopParam,
154
164
  outerLoopParamBindings: loopParamBindings,
165
+ outerLoopIndex: loopIndex,
155
166
  wrapOuter: wrap,
156
167
  }),
157
168
  nestedConditionals: buildLoopChildConditionalsPlan({
@@ -160,6 +171,7 @@ function buildOuterArm(
160
171
  wrap,
161
172
  loopParam,
162
173
  loopParamBindings,
174
+ loopIndex,
163
175
  }),
164
176
  attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
165
177
  texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
@@ -1,13 +1,19 @@
1
1
  /**
2
2
  * Plan types for inner loops emitted inside a composite loop's renderItem
3
- * body. Each `InnerLoopPlan` is one level (= one `mapArray` for reactive
4
- * inner loops, one `forEach` for static ones); `childLevels` recurses for
5
- * deeper nestings.
3
+ * body. Each `InnerLoopPlan` is one level (= one `mapArray`); `childLevels`
4
+ * recurses for deeper nestings.
6
5
  *
7
6
  * Replaces the legacy `emitInnerLoopSetup` + `DepthLevel` walk. The
8
- * builder pre-resolves every per-level decision (mode, container query,
9
- * wrapped array / template / key, narrowed-outer-param children) so the
7
+ * builder pre-resolves every per-level decision (container query, wrapped
8
+ * array / template / key, narrowed-outer-param children) so the
10
9
  * stringifier is a deterministic walk.
10
+ *
11
+ * Every level gets the reactive `mapArray` shape (#2865) — there used to
12
+ * be a second "static forEach, setup-only" shape for a nested loop whose
13
+ * array didn't reference the outer item, but that gate answered the wrong
14
+ * question (it never checked whether the array was reactive at all, only
15
+ * whether it mentioned the outer item's name) and silently froze any other
16
+ * reactivity in the row. See `build-inner-loop.ts`'s module docstring.
11
17
  */
12
18
 
13
19
  import type { LoopChildEvent } from '../../types.ts'
@@ -64,20 +70,9 @@ export interface InnerLoopReactiveAttr {
64
70
  }
65
71
 
66
72
  /**
67
- * One inner loop level inside a composite renderItem.
68
- *
69
- * The `mode` discriminator selects the emission shape:
70
- *
71
- * - `reactive`: full `mapArray(...)` with renderItem (template clone,
72
- * key attr, components/events with inner-wrapped props, reactive text
73
- * effects, recursive childLevels).
74
- * - `static`: `forEach(...)` for setup-only (children are rendered
75
- * into the SSR HTML); components/events use the raw param, no
76
- * wrap, no template clone.
77
- *
78
- * Choosing the mode requires checking whether `inner.array` references
79
- * the outer loop param at *this* level (legacy O-8 fix). The builder
80
- * does that check; the stringifier just dispatches on `mode`.
73
+ * One inner loop level inside a composite renderItem: a full `mapArray(...)`
74
+ * with renderItem (template clone, key attr, components/events with
75
+ * inner-wrapped props, reactive text/attr effects, recursive childLevels).
81
76
  */
82
77
  export interface InnerLoopPlan {
83
78
  /** Unique suffix used in `__ic` / `__innerEl` / `__innerIdx` var names. */
@@ -101,21 +96,25 @@ export interface InnerLoopPlan {
101
96
  * `// Initialize <array> loop components and events` comments.
102
97
  */
103
98
  arraySrc: string
104
- /** Inner loop parameter identifier (used by static forEach). */
99
+ /** Inner loop parameter identifier. */
105
100
  param: string
106
101
  /** Depth used by `keyAttrName(...)`. Same as the IR's `inner.depth`. */
107
102
  keyDepth: number
108
- /**
109
- * Per-mode emission data. Discriminator keeps each shape's required fields
110
- * narrowed and prevents accidental cross-talk.
111
- */
112
- emit: InnerLoopReactiveEmit | InnerLoopStaticEmit
103
+ /** Emission data for this level's `mapArray` renderItem. */
104
+ emit: InnerLoopReactiveEmit
113
105
  /** Recursive child levels — narrowed-outer-param at build time. */
114
106
  childLevels: readonly InnerLoopPlan[]
115
107
  /** Outer loop param threaded into emitComponentAndEventSetup. */
116
108
  outerLoopParam: string | undefined
117
109
  /** Outer loop param destructuring metadata. */
118
110
  outerLoopParamBindings?: readonly LoopParamBinding[]
111
+ /**
112
+ * Outer loop's index param name, when present (#2861) — threaded into
113
+ * `emitComponentAndEventSetup` so a component prop / event handler in
114
+ * this row that closes over the OUTER loop's index reads it through the
115
+ * live accessor instead of a value frozen at row-creation time.
116
+ */
117
+ outerLoopIndex?: string | null
119
118
  }
120
119
 
121
120
  export interface InnerLoopReactiveEmit {
@@ -164,24 +163,4 @@ export interface InnerLoopReactiveEmit {
164
163
  childRefs: readonly LoopChildRefBinding[]
165
164
  }
166
165
 
167
- export interface InnerLoopStaticEmit {
168
- mode: 'static'
169
- /** Raw key expression (used as-is in setAttribute) — null when no key. */
170
- rawKey: string | null
171
- /**
172
- * Body-entry statements (see `PreludeStatements`) emitted at the top of
173
- * the static `forEach(...)` body, after the existence guard. Holds the
174
- * inner `.map()` callback preamble locals (#1064). Emitted unwrapped
175
- * because the forEach param is the literal item, not a signal accessor —
176
- * no accessor rewrite is needed.
177
- */
178
- preludeStatements: PreludeStatements
179
- /** Raw components (no inner-wrap; the static body has no signal accessor). */
180
- components: readonly IRLoopChildComponent[]
181
- /** Raw events (no inner-wrap). */
182
- events: readonly LoopChildEvent[]
183
- /** Imperative ref callbacks (unwrapped — static body has no signal accessor). */
184
- childRefs: readonly LoopChildRefBinding[]
185
- }
186
-
187
166
  export type InnerLoopsPlan = readonly InnerLoopPlan[]
@@ -146,6 +146,13 @@ export interface BranchInnerLoop {
146
146
  outerLoopParam: string
147
147
  /** Outer loop param destructuring metadata. */
148
148
  outerLoopParamBindings?: readonly import('../../../types.ts').LoopParamBinding[]
149
+ /**
150
+ * Outer loop's index param name, when present (#2861) — threaded into
151
+ * `emitComponentAndEventSetup`'s own outer-wrap pass alongside
152
+ * `outerLoopParam`/`outerLoopParamBindings`, mirroring `InnerLoopPlan`'s
153
+ * field of the same name.
154
+ */
155
+ outerLoopIndex?: string | null
149
156
  }
150
157
 
151
158
  export type BranchInnerLoopsPlan = readonly BranchInnerLoop[]
@@ -206,4 +213,11 @@ export interface LoopChildConditionalPlan {
206
213
  whenFalseTemplateHtml: string
207
214
  whenTrueArm: LoopChildArmPlan
208
215
  whenFalseArm: LoopChildArmPlan
216
+ /**
217
+ * `wrappedCondition` reads a `.map()` callback preamble local (#2596,
218
+ * same field as `NestedConditionalPlan.readsPreamble` one level up) —
219
+ * the stringifier must run the row's preamble inside the getter passed
220
+ * to `insert()`; the local isn't otherwise in scope there.
221
+ */
222
+ readsPreamble?: boolean
209
223
  }
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Stringify `InnerLoopsPlan` into source lines.
3
3
  *
4
- * Two emission shapes (mode-discriminated, byte-identical to the legacy
5
- * `emitInnerLoopSetup`):
4
+ * One emission shape (#2865 the former second "static forEach,
5
+ * setup-only" shape for a nested loop whose array didn't reference the
6
+ * outer item was removed: it answered "does the array mention the outer
7
+ * item" instead of "is this reactive at all", so it silently froze any
8
+ * other reactivity — a signal, a memo, a prop, a preamble local — the
9
+ * inner array or its rows depended on):
6
10
  *
7
11
  * reactive — full mapArray:
8
12
  * <indent>// Reactive inner loop: <arraySrc>
@@ -17,23 +21,11 @@
17
21
  * <indent> per-item conditionals: insert() over __innerEl<uid> (#2706)
18
22
  * <indent> return __innerEl<uid>
19
23
  * <indent>}) }
20
- *
21
- * static — forEach setup-only:
22
- * <indent>// Initialize <arraySrc> loop components and events
23
- * <indent>{ const __ic<uid> = <containerExpr>
24
- * <indent>if (__ic<uid> && <arrayExpr>) <arrayExpr>.forEach((<param>, __innerIdx<uid>) => {
25
- * <indent> const __innerEl<uid> = __ic<uid>.children[__innerIdx<uid>]
26
- * <indent> if (!__innerEl<uid>) return
27
- * <indent> <preludeStatements*> // raw preamble (#1064)
28
- * <indent> __innerEl<uid>.setAttribute('<keyAttr>', String(<rawKey>))?
29
- * <indent> emitComponentAndEventSetup(...)
30
- * <indent> recurse on childLevels
31
- * <indent>}) }
32
24
  */
33
25
 
34
26
  import { keyAttrName, mapArrayKeyArgs, profileBindingId, varSlotId } from '../../utils.ts'
35
27
  import { emitComponentAndEventSetup } from '../shared.ts'
36
- import { emitAttrUpdate } from '../../emit-reactive.ts'
28
+ import { emitDedupedAttrUpdate, DEDUP_STORE_DECL } from '../../emit-reactive.ts'
37
29
  import { emitMultiRootTemplateCloneLines, namespaceWrapForTemplate } from './template-parse.ts'
38
30
  import { emitLoopChildRefs } from './loop.ts'
39
31
  import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
@@ -50,18 +42,13 @@ export function stringifyInnerLoops(
50
42
  pc?: string,
51
43
  ): void {
52
44
  for (const inner of plan) {
53
- if (inner.emit.mode === 'reactive') {
54
- emitReactive(lines, inner, indent, pc)
55
- } else {
56
- emitStatic(lines, inner, indent, pc)
57
- }
45
+ emitReactive(lines, inner, indent, pc)
58
46
  }
59
47
  }
60
48
 
61
49
  function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc: string | undefined): void {
62
50
  const uid = inner.uidSuffix
63
51
  const emit = inner.emit
64
- if (emit.mode !== 'reactive') return // narrow
65
52
 
66
53
  lines.push(`${indent}// Reactive inner loop: ${inner.arraySrc}`)
67
54
  lines.push(`${indent}{ const __ic${uid} = ${inner.containerExpr}`)
@@ -108,6 +95,8 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
108
95
  [...emit.events],
109
96
  inner.outerLoopParam,
110
97
  inner.outerLoopParamBindings,
98
+ false,
99
+ inner.outerLoopIndex,
111
100
  )
112
101
  }
113
102
  if (inner.childLevels.length > 0) {
@@ -132,11 +121,13 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
132
121
  lines.push(`${indent} createEffect(() => { ${writer}('${text.slotId}', String(${text.wrappedExpression})) }${profileBindingId(pc, text.slotId)})`)
133
122
  }
134
123
  }
124
+ if (emit.reactiveAttrs.length > 0) lines.push(`${indent} ${DEDUP_STORE_DECL}`)
125
+ let attrOrdinal = 0
135
126
  for (const attr of emit.reactiveAttrs) {
136
127
  const targetVar = `__ta_${attr.slotId.replace(/[^a-zA-Z0-9]/g, '_')}`
137
128
  lines.push(`${indent} { const ${targetVar} = qsa(__innerEl${uid}, '[bf="${attr.slotId}"]')`)
138
129
  lines.push(`${indent} if (${targetVar}) createEffect(() => {`)
139
- for (const stmt of emitAttrUpdate(targetVar, attr.attrName, attr.wrappedExpression, attr.meta)) {
130
+ for (const stmt of emitDedupedAttrUpdate(targetVar, attr.attrName, attr.wrappedExpression, attr.meta, attrOrdinal++)) {
140
131
  lines.push(`${indent} ${stmt}`)
141
132
  }
142
133
  lines.push(`${indent} }${profileBindingId(pc, attr.slotId)}) }`)
@@ -146,7 +137,13 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
146
137
  // ternary. Mirrors `stringifyBranchInnerLoops`'s identical call for a
147
138
  // branch-scoped inner loop's own conditionals.
148
139
  if (emit.conditionals.length > 0) {
149
- stringifyLoopChildConditionals(lines, emit.conditionals, `${indent} `, pc)
140
+ // This inner loop's own preamble (`preludeStatements` above) is not a
141
+ // single re-runnable string like the outer row's `mapPreambleWrapped`,
142
+ // so there is nothing correct to thread here — `readsPreamble &&
143
+ // mapPreambleWrapped`'s guard downstream makes this `undefined` a
144
+ // no-op, exactly as before this fix (inner-loop preambles are out of
145
+ // scope — see PR notes on #2596 follow-up).
146
+ stringifyLoopChildConditionals(lines, emit.conditionals, `${indent} `, pc, undefined)
150
147
  }
151
148
  // Imperative ref callbacks fire on every renderItem invocation, which
152
149
  // means every mount: SSR hydration, initial CSR creation, and same-key
@@ -163,46 +160,3 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
163
160
  // instead of the default `data-key`.
164
161
  lines.push(`${indent}}, '${inner.markerId}'${mapArrayKeyArgs(profileBindingId(pc, inner.slotId), !!emit.wrappedKey, inner.keyDepth)}) }`)
165
162
  }
166
-
167
- function emitStatic(lines: string[], inner: InnerLoopPlan, indent: string, pc: string | undefined): void {
168
- const uid = inner.uidSuffix
169
- const emit = inner.emit
170
- if (emit.mode !== 'static') return
171
-
172
- lines.push(`${indent}// Initialize ${inner.arraySrc} loop components and events`)
173
- lines.push(`${indent}{ const __ic${uid} = ${inner.containerExpr}`)
174
- // Guard: inner array may be undefined when inside a conditional branch.
175
- lines.push(`${indent}if (__ic${uid} && ${inner.arrayExpr}) ${inner.arrayExpr}.forEach((${inner.param}, __innerIdx${uid}) => {`)
176
- lines.push(`${indent} const __innerEl${uid} = __ic${uid}.children[__innerIdx${uid}]`)
177
- lines.push(`${indent} if (!__innerEl${uid}) return`)
178
- // Body-entry statements: inner-`.map()` preamble locals (raw, since the
179
- // forEach param is the literal item — #1064). Emitted before component
180
- // and event setup so their prop getters / handlers resolve the locals.
181
- for (const stmt of emit.preludeStatements) {
182
- lines.push(`${indent} ${stmt}`)
183
- }
184
- if (emit.rawKey) {
185
- lines.push(`${indent} __innerEl${uid}.setAttribute('${keyAttrName(inner.keyDepth)}', String(${emit.rawKey}))`)
186
- }
187
- emitComponentAndEventSetup(
188
- lines,
189
- `${indent} `,
190
- `__innerEl${uid}`,
191
- [...emit.components],
192
- [...emit.events],
193
- inner.outerLoopParam,
194
- inner.outerLoopParamBindings,
195
- )
196
- if (inner.childLevels.length > 0) {
197
- stringifyInnerLoops(lines, inner.childLevels, `${indent} `, pc)
198
- }
199
- // Imperative ref callbacks for static inner loops — fire once per
200
- // forEach iteration (#1244). Static arrays don't reactively re-iterate,
201
- // so this is effectively a one-shot per item.
202
- emitLoopChildRefs(lines, emit.childRefs, {
203
- indent: `${indent} `,
204
- elVar: `__innerEl${uid}`,
205
- bodyIsMultiRoot: false,
206
- })
207
- lines.push(`${indent}}) }`)
208
- }
@@ -29,7 +29,7 @@
29
29
  */
30
30
 
31
31
  import { emitRefCall, varSlotId } from '../../utils.ts'
32
- import { emitAttrUpdate } from '../../emit-reactive.ts'
32
+ import { emitDedupedAttrUpdate, DEDUP_STORE_DECL } from '../../emit-reactive.ts'
33
33
  import type { InsertPlan, InsertArm, ArmBody, ScopeRef } from '../plan/types.ts'
34
34
  import { stringifyBranchLoops } from './branch-loop.ts'
35
35
  import { emitListenerLine } from './event-listener.ts'
@@ -163,10 +163,12 @@ function emitArmBody(
163
163
  const v = varSlotId(slotId)
164
164
  const elVar = `__ra_${v}`
165
165
  lines.push(`${indent}{ const ${elVar} = qsa(__branchScope, '[bf="${slotId}"]')`)
166
+ lines.push(`${indent}${DEDUP_STORE_DECL}`)
166
167
  lines.push(`${indent}if (${elVar}) {`)
168
+ let ordinal = 0
167
169
  for (const attr of attrs) {
168
170
  lines.push(`${indent} __disposers.push(createDisposableEffect(() => {`)
169
- for (const stmt of emitAttrUpdate(elVar, attr.attrName, attr.expression, attr)) {
171
+ for (const stmt of emitDedupedAttrUpdate(elVar, attr.attrName, attr.expression, attr, ordinal++)) {
170
172
  lines.push(`${indent} ${stmt}`)
171
173
  }
172
174
  lines.push(`${indent} }${bindingBfId(slotId)}))`)
@@ -60,7 +60,7 @@
60
60
  */
61
61
 
62
62
  import { isBooleanAttr } from '../../../html-constants.ts'
63
- import { emitAttrUpdate } from '../../emit-reactive.ts'
63
+ import { emitDedupedAttrUpdate, dedupGuard } from '../../emit-reactive.ts'
64
64
  import { toHtmlAttrName } from '../../utils.ts'
65
65
  import type { SkeletonSlotPaths } from '../../html-template.ts'
66
66
  import { claimPlanLiteral, type ClaimSlotSpec } from './claim-plan.ts'
@@ -458,11 +458,13 @@ function emitConditional(
458
458
  lines.push(`${ind}} }`)
459
459
  }
460
460
 
461
- /** `entry.last`-backed dedup test. `in` (not a truthiness check) so a
462
- * legitimately `undefined` value still records and still writes once. */
463
- function dedupGuard(ordinal: number): string {
464
- return `!(${ordinal} in __l) || !Object.is(__l[${ordinal}], __x)`
465
- }
461
+ // `dedupGuard` (the `entry.last`-backed dedup test) and the actual guarded
462
+ // write now live in `emit-reactive.ts` as `emitDedupedAttrUpdate` (#2869)
463
+ // shared with every eager reactive-attr emission site. This module's only
464
+ // remaining substrate-specific piece is WHERE `__l` comes from: the
465
+ // reconciler's `entry.last` (see the per-mode `applyItem`/`applyOuter`
466
+ // callers below), not a closure-local `const __l = []`, because those are
467
+ // separate invocations with no shared closure of their own to own it.
466
468
 
467
469
  function emitAttrBinding(
468
470
  lines: string[],
@@ -474,21 +476,16 @@ function emitAttrBinding(
474
476
  // plain read. An ADOPTED row starts with an EMPTY `refs` array and each
475
477
  // binding claims its OWN slot on first use — see `elementAccess`.
476
478
  const target = mode === 'create' ? `__r[${a.refIndex}]` : elementAccess(a)
477
- lines.push(`${ind}{ const __t = ${target}`)
478
- lines.push(`${ind}if (__t) {`)
479
- lines.push(`${ind} const __x = ${a.wrappedExpression}`)
480
479
  const guard = mode === 'create'
481
480
  ? null
482
481
  : mode === 'item'
483
482
  ? dedupGuard(a.ordinal)
484
483
  : `__seed ? (${seedDiffersExpr('__t', a)}) : (${dedupGuard(a.ordinal)})`
485
- const writeIndent = guard ? `${ind} ` : `${ind} `
486
- if (guard) lines.push(`${ind} if (${guard}) {`)
487
- for (const stmt of emitAttrUpdate('__t', a.attrName, '__x', a.meta)) {
488
- lines.push(`${writeIndent}${stmt}`)
484
+ lines.push(`${ind}{ const __t = ${target}`)
485
+ lines.push(`${ind}if (__t) {`)
486
+ for (const stmt of emitDedupedAttrUpdate('__t', a.attrName, a.wrappedExpression, a.meta, a.ordinal, guard)) {
487
+ lines.push(`${ind} ${stmt}`)
489
488
  }
490
- if (guard) lines.push(`${ind} }`)
491
- lines.push(`${ind} __l[${a.ordinal}] = __x`)
492
489
  lines.push(`${ind}} }`)
493
490
  }
494
491