@barefootjs/jsx 0.32.0 → 0.33.1

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/adapters/dangerous-inner-html.d.ts.map +1 -1
  2. package/dist/adapters/parsed-expr-emitter.d.ts +22 -0
  3. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  4. package/dist/analyzer.d.ts.map +1 -1
  5. package/dist/errors.d.ts +1 -0
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/expression-parser.d.ts +29 -6
  8. package/dist/expression-parser.d.ts.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +274 -80
  12. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  13. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +8 -0
  15. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts +12 -0
  18. package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +1 -0
  20. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  24. package/dist/ir-to-client-js/imports.d.ts +2 -2
  25. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  26. package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/types.d.ts +21 -0
  28. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  29. package/dist/query-href-lowering.d.ts.map +1 -1
  30. package/dist/ssr-seed-plan.d.ts.map +1 -1
  31. package/dist/static-literal.d.ts.map +1 -1
  32. package/dist/to-locale-date-lowering.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +2 -4
  35. package/src/__tests__/child-components-in-map.test.ts +11 -3
  36. package/src/__tests__/client-js-generation.test.ts +37 -1
  37. package/src/__tests__/expression-parser.test.ts +43 -6
  38. package/src/__tests__/inline-jsx-callback.test.ts +55 -0
  39. package/src/__tests__/ir-jsx-props.test.ts +148 -0
  40. package/src/__tests__/issue-2705-branch-inner-loop-container.test.ts +91 -0
  41. package/src/__tests__/markup-prop-brand.test.ts +49 -0
  42. package/src/__tests__/nested-loop-conditional.test.ts +20 -11
  43. package/src/__tests__/return-through-local-var.test.ts +269 -0
  44. package/src/__tests__/serialize-parsed-expr.test.ts +17 -3
  45. package/src/__tests__/ssr-defaults.test.ts +41 -0
  46. package/src/__tests__/ssr-seed-plan.test.ts +12 -2
  47. package/src/adapters/dangerous-inner-html.ts +1 -0
  48. package/src/adapters/parsed-expr-emitter.ts +49 -3
  49. package/src/analyzer.ts +71 -0
  50. package/src/errors.ts +17 -1
  51. package/src/expression-parser.ts +200 -89
  52. package/src/index.ts +2 -2
  53. package/src/ir-to-client-js/collect-elements.ts +31 -20
  54. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +19 -0
  55. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +22 -3
  56. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
  57. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +12 -0
  58. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +9 -0
  59. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +7 -1
  60. package/src/ir-to-client-js/element-refs.ts +8 -0
  61. package/src/ir-to-client-js/emit-reactive.ts +91 -23
  62. package/src/ir-to-client-js/imports.ts +5 -0
  63. package/src/ir-to-client-js/index.ts +4 -0
  64. package/src/ir-to-client-js/phases/provider-and-child-inits.ts +5 -1
  65. package/src/ir-to-client-js/types.ts +21 -0
  66. package/src/jsx-to-ir.ts +161 -9
  67. package/src/query-href-lowering.ts +4 -0
  68. package/src/rich-type-refusal.ts +1 -1
  69. package/src/ssr-defaults.ts +51 -2
  70. package/src/ssr-seed-plan.ts +20 -3
  71. package/src/static-literal.ts +15 -1
  72. package/src/to-locale-date-lowering.ts +4 -0
@@ -30,6 +30,8 @@ import {
30
30
  } from '../../utils.ts'
31
31
  import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
32
32
  import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
33
+ import { buildLoopChildConditionalsPlan } from './build-loop-child-arm.ts'
34
+ import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
33
35
 
34
36
  /**
35
37
  * Mirror of the helper in `build-loop-child-arm.ts` — kept local to avoid
@@ -160,6 +162,7 @@ function buildReactiveEmit(
160
162
  outerLoopParamBindings?: readonly LoopParamBinding[],
161
163
  ): InnerLoopReactiveEmit {
162
164
  const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
165
+ const wrapBoth = (expr: string) => wrapLoopParamAsAccessor(wrapOuter(expr), inner.param, inner.paramBindings)
163
166
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
164
167
  const wrappedKey = inner.key
165
168
  ? wrapLoopParamAsAccessor(inner.key, inner.param, inner.paramBindings)
@@ -249,6 +252,21 @@ function buildReactiveEmit(
249
252
 
250
253
  const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
251
254
 
255
+ // Per-item conditionals inside THIS loop's own row (#2706) — same
256
+ // insert()-parity treatment the top-level loop's row conditionals
257
+ // already get (`buildLoopReactiveEffectsPlan`), now extended to a
258
+ // NESTED loop's row too. `scopeVar` is the row's own element
259
+ // (`__innerEl<uidSuffix>`, matching `stringifyInnerLoops`'s emission),
260
+ // and `wrapBoth` matches every other per-item expression in this emit
261
+ // (outer accessor, then inner accessor).
262
+ const conditionals: LoopChildConditionalPlan[] = buildLoopChildConditionalsPlan({
263
+ conditionals: inner.bindings.conditionals,
264
+ scopeVar: `__innerEl${uidSuffix}`,
265
+ wrap: wrapBoth,
266
+ loopParam: inner.param,
267
+ loopParamBindings: inner.paramBindings,
268
+ })
269
+
252
270
  return {
253
271
  mode: 'reactive',
254
272
  keyFn: loopKeyFn(inner),
@@ -261,6 +279,7 @@ function buildReactiveEmit(
261
279
  events,
262
280
  reactiveTexts,
263
281
  reactiveAttrs,
282
+ conditionals,
264
283
  childRefs,
265
284
  }
266
285
  }
@@ -190,6 +190,14 @@ export interface BuildBranchInnerLoopsArgs {
190
190
  innerLoops: readonly NestedLoop[] | undefined
191
191
  /** The variable expression naming the parent scope element (e.g. `__branchScope`). */
192
192
  scopeVar: string
193
+ /**
194
+ * The enclosing conditional's own slot id — every call site of this
195
+ * builder originates from a conditional branch's arm, so this is always
196
+ * a real id. Used as the `containerExpr` fallback (`findCondContainer`,
197
+ * #2705) for an inner loop whose IR never got a `containerSlotId` of its
198
+ * own (its wrapper element sits outside the branch's IR subtree).
199
+ */
200
+ condSlotId: string
193
201
  /** Outer loop param identifier (the conditional's enclosing loop). */
194
202
  outerLoopParam: string
195
203
  /** Outer loop param destructuring metadata. */
@@ -214,6 +222,7 @@ export function buildBranchInnerLoopsPlan(
214
222
  const {
215
223
  innerLoops,
216
224
  scopeVar,
225
+ condSlotId,
217
226
  outerLoopParam,
218
227
  outerLoopParamBindings,
219
228
  wrapOuter,
@@ -230,10 +239,15 @@ export function buildBranchInnerLoopsPlan(
230
239
 
231
240
  const csl = inner.containerSlotId
232
241
  // Inner loop's container: host-side `bf="<slot>"` slot marker first,
233
- // then (bf-h, bf-m) when the container is itself a child scope.
242
+ // then (bf-h, bf-m) when the container is itself a child scope. When
243
+ // neither exists — the loop's own IR never got a `containerSlotId`
244
+ // because its wrapper element sits outside the branch's IR subtree
245
+ // (#2705) — resolve via the conditional's OWN comment marker instead
246
+ // of falling back to the whole branch scope, which may be several
247
+ // elements wider than the loop's actual container.
234
248
  const containerExpr = csl
235
249
  ? `(${scopeVar}.querySelector('[bf="${csl}"]') ?? ${scopeVar}.querySelector(\`[${BF_HOST}="\${__scopeId}"][${BF_AT}="${csl}"]\`) ?? ${scopeVar})`
236
- : scopeVar
250
+ : `findCondContainer(${scopeVar}, '${condSlotId}')`
237
251
 
238
252
  const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
239
253
  const wrappedKey = inner.key
@@ -372,12 +386,14 @@ export function buildLoopChildConditionalsPlan(
372
386
  wrap,
373
387
  loopParam,
374
388
  loopParamBindings,
389
+ condId: cond.slotId,
375
390
  }),
376
391
  whenFalseArm: buildLoopChildArmPlan({
377
392
  branch: cond.whenFalse,
378
393
  wrap,
379
394
  loopParam,
380
395
  loopParamBindings,
396
+ condId: cond.slotId,
381
397
  }),
382
398
  })
383
399
  }
@@ -439,10 +455,12 @@ interface BuildLoopChildArmArgs {
439
455
  wrap: (expr: string) => string
440
456
  loopParam: string
441
457
  loopParamBindings?: readonly LoopParamBinding[]
458
+ /** The enclosing conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
459
+ condId: string
442
460
  }
443
461
 
444
462
  function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
445
- const { branch, wrap, loopParam, loopParamBindings } = args
463
+ const { branch, wrap, loopParam, loopParamBindings, condId } = args
446
464
  return {
447
465
  events: buildBranchEventBindingsPlan({
448
466
  events: branch.events,
@@ -455,6 +473,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
455
473
  innerLoops: buildBranchInnerLoopsPlan({
456
474
  innerLoops: branch.innerLoops,
457
475
  scopeVar: '__branchScope',
476
+ condSlotId: condId,
458
477
  outerLoopParam: loopParam,
459
478
  outerLoopParamBindings: loopParamBindings,
460
479
  wrapOuter: wrap,
@@ -110,8 +110,8 @@ export function buildReactiveEffectsPlan(
110
110
  wrappedCondition: wrap(cond.condition),
111
111
  whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
112
112
  whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
113
- whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, profileComponentName),
114
- whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, profileComponentName),
113
+ whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
114
+ whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, cond.slotId, profileComponentName),
115
115
  ...(cond.readsPreamble && { readsPreamble: true }),
116
116
  })
117
117
  }
@@ -130,6 +130,8 @@ function buildOuterArm(
130
130
  wrap: (expr: string) => string,
131
131
  loopParam: string,
132
132
  loopParamBindings: readonly LoopParamBinding[] | undefined,
133
+ /** The conditional's own slot id — threaded to `buildBranchInnerLoopsPlan`'s `condSlotId` (#2705). */
134
+ condSlotId: string,
133
135
  profileComponentName?: string,
134
136
  ): LoopChildArmPlan {
135
137
  return {
@@ -145,6 +147,7 @@ function buildOuterArm(
145
147
  innerLoops: buildBranchInnerLoopsPlan({
146
148
  innerLoops: branch.innerLoops,
147
149
  scopeVar: '__branchScope',
150
+ condSlotId,
148
151
  outerLoopParam: loopParam,
149
152
  outerLoopParamBindings: loopParamBindings,
150
153
  wrapOuter: wrap,
@@ -17,6 +17,7 @@ import type {
17
17
  LoopParamBinding,
18
18
  } from '../../../types.ts'
19
19
  import type { LoopChildRefBinding } from './loop.ts'
20
+ import type { LoopChildConditionalPlan } from './loop-child-arm.ts'
20
21
 
21
22
  /**
22
23
  * Body-entry statements emitted in order at the top of a `mapArray`
@@ -148,6 +149,17 @@ export interface InnerLoopReactiveEmit {
148
149
  reactiveTexts: readonly InnerLoopText[]
149
150
  /** Pre-wrapped reactive attribute effects for the inner-item body. */
150
151
  reactiveAttrs: readonly InnerLoopReactiveAttr[]
152
+ /**
153
+ * Per-item conditionals inside THIS loop's own row (#2706) — each gets
154
+ * its own `insert()` call, the same insert()-parity the top-level loop's
155
+ * row conditionals already have (`ReactiveEffectsPlan.conditionals`).
156
+ * Before this field existed, a per-item conditional inside a nested
157
+ * loop's row was baked into the static row template ONCE at row
158
+ * creation and never revisited — silently frozen against later signal
159
+ * changes — while its reactive text still (unsoundly) assumed insert()
160
+ * kept the branch's marker around.
161
+ */
162
+ conditionals: readonly LoopChildConditionalPlan[]
151
163
  /** Pre-wrapped imperative ref callbacks for the inner-item body (#1244). */
152
164
  childRefs: readonly LoopChildRefBinding[]
153
165
  }
@@ -14,6 +14,7 @@
14
14
  * <indent> emitComponentAndEventSetup(...)
15
15
  * <indent> recurse on childLevels
16
16
  * <indent> reactive text effects
17
+ * <indent> per-item conditionals: insert() over __innerEl<uid> (#2706)
17
18
  * <indent> return __innerEl<uid>
18
19
  * <indent>}) }
19
20
  *
@@ -36,6 +37,7 @@ import { emitAttrUpdate } from '../../emit-reactive.ts'
36
37
  import { emitMultiRootTemplateCloneLines, namespaceWrapForTemplate } from './template-parse.ts'
37
38
  import { emitLoopChildRefs } from './loop.ts'
38
39
  import { claimPlanLiteral, claimWriterVarName, type ClaimSlotSpec } from './claim-plan.ts'
40
+ import { stringifyLoopChildConditionals } from './loop-child-arm.ts'
39
41
  import type {
40
42
  InnerLoopPlan,
41
43
  InnerLoopsPlan,
@@ -139,6 +141,13 @@ function emitReactive(lines: string[], inner: InnerLoopPlan, indent: string, pc:
139
141
  }
140
142
  lines.push(`${indent} }${profileBindingId(pc, attr.slotId)}) }`)
141
143
  }
144
+ // Per-item conditionals inside THIS loop's own row (#2706) — each is a
145
+ // real `insert()` over `__innerEl<uid>`, not a bake-once-at-creation
146
+ // ternary. Mirrors `stringifyBranchInnerLoops`'s identical call for a
147
+ // branch-scoped inner loop's own conditionals.
148
+ if (emit.conditionals.length > 0) {
149
+ stringifyLoopChildConditionals(lines, emit.conditionals, `${indent} `, pc)
150
+ }
142
151
  // Imperative ref callbacks fire on every renderItem invocation, which
143
152
  // means every mount: SSR hydration, initial CSR creation, and same-key
144
153
  // remount after unmount (#1244).
@@ -553,7 +553,13 @@ function seedDiffersExpr(target: string, a: LazyRowAttrBinding): string {
553
553
  if (a.attrName === 'dangerouslySetInnerHTML' || html === 'dangerouslySetInnerHTML') return 'true'
554
554
  if (html === 'style') return `${target}.getAttribute('style') !== styleToCss(__x)`
555
555
  if (html === 'class') return `${target}.getAttribute('class') !== (__x != null ? String(__x) : null)`
556
- if (html === 'value') return `${target}.value !== String(__x)`
556
+ // Mirrors `emitValueUpdateStatements`'s runtime `'value' in target` gate
557
+ // (#2716): a target with no native `.value` property never gets one
558
+ // written, so the seed-diff must compare against the ATTRIBUTE it would
559
+ // actually receive there, not the (absent) property.
560
+ if (html === 'value') {
561
+ return `('value' in ${target} ? ${target}.value !== String(__x) : ${target}.getAttribute('value') !== String(__x))`
562
+ }
557
563
  if (isBooleanAttr(html)) return `${target}.${html} !== !!(__x)`
558
564
  if (a.meta.presenceOrUndefined) {
559
565
  // Compare the VALUE the writer would produce, not just presence.
@@ -71,6 +71,14 @@ export function generateElementRefs(ctx: ClientJsContext): string {
71
71
  regularSlots.delete(slotId)
72
72
  }
73
73
 
74
+ // The component's own `comment: true` root slot needs no `$c` ref at
75
+ // all — its consumers (provider-and-child-inits.ts, emit-reactive.ts)
76
+ // reference `__scope` directly instead (#2649, see
77
+ // `ClientJsContext.commentScopeRootSlotId`'s docstring).
78
+ if (ctx.commentScopeRootSlotId) {
79
+ componentSlots.delete(ctx.commentScopeRootSlotId)
80
+ }
81
+
74
82
  if (regularSlots.size === 0 && componentSlots.size === 0) return ''
75
83
 
76
84
  const refLines: string[] = []
@@ -28,6 +28,57 @@ function bindingIdArg(ctx: ClientJsContext, slotId: string | undefined): string
28
28
  return `, ${JSON.stringify(`${ctx.componentName}#binding:${slotId}`)}`
29
29
  }
30
30
 
31
+ /**
32
+ * Generate statements that write a `value` HTML ATTRIBUTE the developer
33
+ * wrote directly on an element (`<div value={x}>`, a loop row's `<li
34
+ * value={x}>`, …) — SSR renders that same attribute, so hydration keeping
35
+ * it in sync is exactly the contract. Gated at runtime to elements that
36
+ * ALREADY expose a native `.value` IDL property (`'value' in target` —
37
+ * form controls, but also e.g. `<li value>`; the same duck-type check
38
+ * `applyRestAttrs` uses, deliberately not a tag-name allowlist), because
39
+ * for those `setAttribute('value', x)` only sets the INITIAL HTML
40
+ * attribute and the live property is required after user interaction; any
41
+ * other element falls back to a plain attribute write, which still matches
42
+ * what SSR rendered there. Writing the live property unconditionally would
43
+ * plant an expando SSR never had — a hydrated/SSR DOM-state divergence and a
44
+ * hazard for anything that duck-types form controls via `'value' in el`
45
+ * (#2716).
46
+ *
47
+ * NOT for the child-component-root `value` MIRROR (`emitReactivePropBindings`
48
+ * / `emitReactiveChildProps` reflecting a named prop onto a child's root
49
+ * element) — that mechanism has no SSR-rendered counterpart at all
50
+ * regardless of prop name, so an attribute fallback there would itself
51
+ * plant a fresh SSR/hydrate divergence; see `emitChildValueMirrorStatements`.
52
+ */
53
+ function emitValueUpdateStatements(target: string, expression: string): string[] {
54
+ return [
55
+ `const __val = String(${expression})`,
56
+ `if ('value' in ${target}) { if (${target}.value !== __val) ${target}.value = __val } else { ${target}.setAttribute('value', __val) }`,
57
+ ]
58
+ }
59
+
60
+ /**
61
+ * `value`-prop write for the CHILD-ROOT MIRROR mechanism
62
+ * (`emitReactivePropBindings` / `emitReactiveChildProps`, both reactively
63
+ * reflect a parent-passed NAMED PROP onto a child component's root DOM
64
+ * element). Unlike a developer-authored `value=` attribute
65
+ * (`emitValueUpdateStatements`), this mirror has NO SSR-rendered
66
+ * counterpart at all — SSR never puts a `value` attribute on a child's
67
+ * root just because the parent passed a `value` prop. So a root WITHOUT a
68
+ * native `.value` property gets NOTHING written — not even an attribute
69
+ * (confirmed against the oracle's structural-HTML comparison, #2716: an
70
+ * attribute fallback here reintroduced a fresh SSR/hydrate divergence one
71
+ * layer down from the IDL-property expando this replaces). A root that
72
+ * already exposes `.value` (`'value' in target`, same duck-type gate as
73
+ * the direct-attribute case) still gets the live controlled-value
74
+ * property.
75
+ */
76
+ function emitChildValueMirrorStatements(target: string, expression: string): string[] {
77
+ return [
78
+ `if ('value' in ${target}) { const __val = String(${expression}); if (${target}.value !== __val) ${target}.value = __val }`,
79
+ ]
80
+ }
81
+
31
82
  /**
32
83
  * Generate JS statements to update a DOM attribute reactively.
33
84
  * Centralizes the attribute-type dispatch (value, class, boolean, presence, generic)
@@ -55,10 +106,7 @@ export function emitAttrUpdate(target: string, attrName: string, expression: str
55
106
  ]
56
107
  }
57
108
  if (htmlName === 'value') {
58
- return [
59
- `const __val = String(${expression})`,
60
- `if (${target}.value !== __val) ${target}.value = __val`,
61
- ]
109
+ return emitValueUpdateStatements(target, expression)
62
110
  }
63
111
  if (isBooleanAttr(htmlName)) {
64
112
  return [`${target}.${htmlName} = !!(${expression})`]
@@ -462,37 +510,43 @@ export function emitReactivePropBindings(lines: string[], ctx: ClientJsContext):
462
510
  }
463
511
 
464
512
  for (const [slotId, props] of propsBySlot) {
465
- const v = varSlotId(slotId)
466
- lines.push(` if (_${v}) {`)
513
+ // The component's own `comment: true` root child IS `__scope` — no
514
+ // `$c` ref was declared for it (element-refs.ts), so reference
515
+ // `__scope` directly instead of a `_sN` var that doesn't exist
516
+ // (#2649, see `ClientJsContext.commentScopeRootSlotId`'s docstring).
517
+ const ref = slotId === ctx.commentScopeRootSlotId ? '__scope' : `_${varSlotId(slotId)}`
518
+ lines.push(` if (${ref}) {`)
467
519
  for (const prop of props) {
468
520
  const value = `${prop.expression}()`
469
521
  if (prop.propName === 'selected') {
470
522
  if (prop.componentName === 'TabsContent') {
471
- lines.push(` _${v}.setAttribute('data-state', ${value} ? 'active' : 'inactive')`)
523
+ lines.push(` ${ref}.setAttribute('data-state', ${value} ? 'active' : 'inactive')`)
472
524
  lines.push(` if (${value}) {`)
473
- lines.push(` _${v}.classList.remove('hidden')`)
525
+ lines.push(` ${ref}.classList.remove('hidden')`)
474
526
  lines.push(` } else {`)
475
- lines.push(` _${v}.classList.add('hidden')`)
527
+ lines.push(` ${ref}.classList.add('hidden')`)
476
528
  lines.push(` }`)
477
529
  } else {
478
530
  // Update data-state and aria-selected attributes.
479
531
  // Visual styling is driven by CSS data-[state=active/inactive]: selectors.
480
- lines.push(` _${v}.setAttribute('aria-selected', String(${value}))`)
481
- lines.push(` _${v}.setAttribute('data-state', ${value} ? 'active' : 'inactive')`)
482
- lines.push(` _${v}.setAttribute('tabindex', ${value} ? '0' : '-1')`)
532
+ lines.push(` ${ref}.setAttribute('aria-selected', String(${value}))`)
533
+ lines.push(` ${ref}.setAttribute('data-state', ${value} ? 'active' : 'inactive')`)
534
+ lines.push(` ${ref}.setAttribute('tabindex', ${value} ? '0' : '-1')`)
483
535
  }
484
- // Use DOM property assignment for value and boolean attrs.
485
- // setAttribute('value', x) only sets the initial HTML attribute; after user
486
- // interaction the DOM property diverges, so .value = x is required.
536
+ // Use DOM property assignment for value and boolean attrs, but only
537
+ // on genuine form controls see `emitChildValueMirrorStatements`'s
538
+ // docstring (#2716). `ref` here is a named prop's MIRROR target
539
+ // element (a child component's arbitrary root), not necessarily a
540
+ // form control, and this mirror has no SSR-rendered counterpart to
541
+ // fall back to.
487
542
  // Boolean attrs (disabled, checked, etc.) treat any attribute presence as
488
543
  // truthy, so setAttribute('disabled', 'false') still disables the element.
489
544
  } else if (prop.propName === 'value') {
490
- lines.push(` const __val = String(${value})`)
491
- lines.push(` if (_${v}.value !== __val) _${v}.value = __val`)
545
+ for (const stmt of emitChildValueMirrorStatements(ref, value)) lines.push(` ${stmt}`)
492
546
  } else if (isBooleanAttr(prop.propName)) {
493
- lines.push(` _${v}.${prop.propName} = !!(${value})`)
547
+ lines.push(` ${ref}.${prop.propName} = !!(${value})`)
494
548
  } else {
495
- lines.push(` _${v}.setAttribute('${prop.propName}', String(${value}))`)
549
+ lines.push(` ${ref}.setAttribute('${prop.propName}', String(${value}))`)
496
550
  }
497
551
  }
498
552
  lines.push(` }`)
@@ -520,13 +574,27 @@ export function emitReactiveChildProps(lines: string[], ctx: ClientJsContext): v
520
574
 
521
575
  for (const [, props] of propsByComponent) {
522
576
  const first = props[0]
577
+ // The component's own `comment: true` root child IS `__scope` — query
578
+ // it directly rather than through `$c`, which cannot tell "I already
579
+ // am this slot" apart from a coincidentally-matching descendant
580
+ // (#2649, see `ClientJsContext.commentScopeRootSlotId`'s docstring).
581
+ const isCommentRoot = first.slotId !== null && first.slotId === ctx.commentScopeRootSlotId
523
582
  const varSuffix = first.slotId ? varSlotId(first.slotId).replace(/-/g, '_') : first.componentName
524
- const varName = `__${first.componentName}_${varSuffix}El`
525
- const selectorArg = first.slotId ? first.slotId : first.componentName
526
- lines.push(` const [${varName}] = $c(__scope, '${selectorArg}')`)
583
+ const varName = isCommentRoot ? '__scope' : `__${first.componentName}_${varSuffix}El`
584
+ if (!isCommentRoot) {
585
+ const selectorArg = first.slotId ? first.slotId : first.componentName
586
+ lines.push(` const [${varName}] = $c(__scope, '${selectorArg}')`)
587
+ }
527
588
  lines.push(` if (${varName}) {`)
528
589
  for (const prop of props) {
529
- for (const stmt of emitAttrUpdate(varName, prop.attrName, prop.expression, prop)) {
590
+ // `value` is the CHILD-ROOT MIRROR case, not a developer-authored
591
+ // attribute — route it through the no-SSR-fallback helper instead
592
+ // of `emitAttrUpdate`'s generic (attribute-fallback) dispatch;
593
+ // see `emitChildValueMirrorStatements`'s docstring (#2716).
594
+ const stmts = toHtmlAttrName(prop.attrName) === 'value'
595
+ ? emitChildValueMirrorStatements(varName, prop.expression)
596
+ : emitAttrUpdate(varName, prop.attrName, prop.expression, prop)
597
+ for (const stmt of stmts) {
530
598
  lines.push(` ${stmt}`)
531
599
  }
532
600
  }
@@ -11,6 +11,11 @@ import { identifierCallPattern } from '../identifier-pattern.ts'
11
11
  export const RUNTIME_IMPORT_CANDIDATES = [
12
12
  'createSignal', 'createMemo', 'createEffect', 'onCleanup', 'onMount',
13
13
  'hydrate', 'insert', 'getLoopChildren', 'getLoopNodes', 'mapArray', 'mapArrayAnchored', 'mapArrayLazy', 'patchLeaf', 'createDisposableEffect',
14
+ // Resolves the real DOM container for a loop nested inside a loop-row
15
+ // conditional's branch when the conditional's wrapper element carries no
16
+ // `bf="<slot>"` marker of its own (#2705) — see `findCondContainer`'s
17
+ // docstring (runtime/insert.ts) for why the marker collector can't see it.
18
+ 'findCondContainer',
14
19
  'createComponent', 'renderChild', 'registerComponent', 'registerTemplate', 'initChild', 'upsertChild',
15
20
  // Connects a template-clone loop row before the body's tail runs, so a child
16
21
  // that inits inside it resolves context against real ancestors rather than
@@ -185,6 +185,10 @@ function createContext(
185
185
  refElements: [],
186
186
  childInits: [],
187
187
  deferredChildSlots: new Set(),
188
+ // Mirrors `emit-registration.ts`'s `isCommentScope` component-root
189
+ // disjunct (not the fragment disjunct — that case is disambiguated at
190
+ // runtime by `commentScopeRegistry`, not by this slot-id shortcut).
191
+ commentScopeRootSlotId: ir.root.type === 'component' ? ir.root.slotId : null,
188
192
  reactiveProps: [],
189
193
  reactiveChildProps: [],
190
194
  reactiveAttrs: [],
@@ -39,7 +39,11 @@ export function emitProviderAndChildInits(lines: string[], ctx: ClientJsContext)
39
39
  lines.push(` upsertChild(__scope, '${registryName}', '${child.slotId}', ${child.propsExpr})`)
40
40
  continue
41
41
  }
42
- const scopeRef = child.slotId ? `_${varSlotId(child.slotId)}` : '__scope'
42
+ // The component's own `comment: true` root child IS `__scope` (no
43
+ // separate DOM node exists for it to be looked up at) — see
44
+ // `ClientJsContext.commentScopeRootSlotId`'s docstring (#2649).
45
+ const isCommentRoot = child.slotId !== null && child.slotId === ctx.commentScopeRootSlotId
46
+ const scopeRef = !child.slotId || isCommentRoot ? '__scope' : `_${varSlotId(child.slotId)}`
43
47
  lines.push(` initChild('${registryName}', ${scopeRef}, ${child.propsExpr})`)
44
48
  }
45
49
  }
@@ -102,6 +102,27 @@ export interface ClientJsContext {
102
102
  * registration-template emit so both agree on which children defer.
103
103
  */
104
104
  deferredChildSlots: Set<string>
105
+ /**
106
+ * Slot id of the component's own root, when the ENTIRE render is a single
107
+ * child component call (`ir.root.type === 'component'` — the IR shape is
108
+ * the sole source of truth; `emit-registration.ts`'s `isCommentScope`
109
+ * derives the def's `comment: true` from the same shape). Such a child
110
+ * never gets its own DOM node: `materializeComponent`/`renderChild` leave
111
+ * `bf-s` unset and the child's markup becomes `__scope` itself (#2649).
112
+ * A generic `$c(__scope, slotId)` lookup for this one slot is therefore
113
+ * not just redundant but actively ambiguous — a genuine grandchild
114
+ * nested inside it can derive a `bf-s` whose suffix collides with this
115
+ * same slot id (see `component.ts`'s `_parentScopeId` push), and
116
+ * `$cSingle` cannot tell "I already am this slot" apart from "a
117
+ * coincidentally-matching descendant is this slot" from the DOM alone.
118
+ * Emission sites that would otherwise query this slot via `$c`
119
+ * (`element-refs.ts`, `provider-and-child-inits.ts`,
120
+ * `emit-reactive.ts`) use `__scope` directly instead, sidestepping the
121
+ * ambiguity entirely rather than trying to make the query precise
122
+ * enough to resolve it. `null` for a component whose root is a regular
123
+ * element/fragment (the overwhelmingly common case).
124
+ */
125
+ commentScopeRootSlotId: string | null
105
126
  reactiveProps: ReactiveComponentProp[]
106
127
  reactiveChildProps: ReactiveChildProp[]
107
128
  reactiveAttrs: ReactiveAttribute[]