@barefootjs/jsx 0.33.0 → 0.33.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 (75) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/errors.d.ts +1 -0
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/expression-parser.d.ts +14 -0
  5. package/dist/expression-parser.d.ts.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +268 -79
  9. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +8 -0
  12. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.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/inner-loop.d.ts +12 -0
  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/stringify/inner-loop.d.ts +1 -0
  17. package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/element-refs.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/html-template.d.ts +7 -7
  23. package/dist/ir-to-client-js/html-template.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/index.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/phases/provider-and-child-inits.d.ts.map +1 -1
  28. package/dist/ir-to-client-js/prop-handling.d.ts +33 -0
  29. package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/reactivity.d.ts +5 -0
  31. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/rewrite-props-object.d.ts +36 -8
  33. package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
  34. package/dist/ir-to-client-js/types.d.ts +21 -0
  35. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  36. package/dist/types.d.ts +14 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +2 -2
  39. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +23 -6
  40. package/src/__tests__/binding-scope-ratchet.test.ts +5 -1
  41. package/src/__tests__/child-components-in-map.test.ts +11 -3
  42. package/src/__tests__/client-js-generation.test.ts +48 -1
  43. package/src/__tests__/inline-jsx-callback.test.ts +55 -0
  44. package/src/__tests__/ir-jsx-props.test.ts +148 -0
  45. package/src/__tests__/issue-2705-branch-inner-loop-container.test.ts +91 -0
  46. package/src/__tests__/issue-2723-prop-alias-reactivity.test.ts +124 -0
  47. package/src/__tests__/markup-prop-brand.test.ts +49 -0
  48. package/src/__tests__/nested-loop-conditional.test.ts +20 -11
  49. package/src/__tests__/return-through-local-var.test.ts +269 -0
  50. package/src/__tests__/rewrite-props-object.test.ts +41 -4
  51. package/src/analyzer.ts +71 -0
  52. package/src/errors.ts +17 -1
  53. package/src/expression-parser.ts +26 -0
  54. package/src/index.ts +1 -1
  55. package/src/ir-to-client-js/collect-elements.ts +49 -45
  56. package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +19 -0
  57. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +22 -3
  58. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +5 -2
  59. package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +12 -0
  60. package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +9 -0
  61. package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +7 -1
  62. package/src/ir-to-client-js/element-refs.ts +8 -0
  63. package/src/ir-to-client-js/emit-reactive.ts +91 -23
  64. package/src/ir-to-client-js/emit-registration.ts +26 -7
  65. package/src/ir-to-client-js/generate-init.ts +1 -1
  66. package/src/ir-to-client-js/html-template.ts +8 -8
  67. package/src/ir-to-client-js/imports.ts +5 -0
  68. package/src/ir-to-client-js/index.ts +10 -4
  69. package/src/ir-to-client-js/phases/provider-and-child-inits.ts +5 -1
  70. package/src/ir-to-client-js/prop-handling.ts +94 -0
  71. package/src/ir-to-client-js/reactivity.ts +59 -0
  72. package/src/ir-to-client-js/rewrite-props-object.ts +50 -10
  73. package/src/ir-to-client-js/types.ts +21 -0
  74. package/src/jsx-to-ir.ts +204 -9
  75. package/src/types.ts +14 -0
package/src/errors.ts CHANGED
@@ -22,11 +22,24 @@ export const ErrorCodes = {
22
22
  // Signal/Memo errors (BF011-BF019)
23
23
  SIGNAL_OUTSIDE_COMPONENT: 'BF011',
24
24
 
25
- // JSX errors (BF021-BF029)
25
+ // JSX errors (BF021-BF029). BF022 was retired (see
26
+ // `invalid-jsx-attribute.audit.test.ts`) and BF026 is reserved by
27
+ // `spec/callback-fidelity.md` for a future `.map()`-callback-shape
28
+ // diagnostic — BF027 is the next free slot.
26
29
  UNSUPPORTED_JSX_PATTERN: 'BF021',
27
30
  MISSING_KEY_IN_LIST: 'BF023',
28
31
  MISSING_KEY_IN_NESTED_LIST: 'BF024',
29
32
  UNSUPPORTED_DESTRUCTURE_REST: 'BF025',
33
+ // The component's return statement resolves to a bare identifier that
34
+ // refers to a local `const`/`let` whose initializer IS JSX (or a
35
+ // JSX-shaped ternary/`&&`/`||`/`??`), e.g. `const __root = <div/>; return
36
+ // __root`. JSX-child position resolves such identifiers through
37
+ // `jsxConstants` / `inlineableJsxConsts` (#547 / #1409), but return
38
+ // position deliberately does not (see `transformExpressionInner`'s
39
+ // docstring) — so the dispatcher's scalar-leaf fallback silently produces
40
+ // no IR and no diagnostic (#2720). Loud stopgap until the analyzer learns
41
+ // to resolve the identifier at return position too.
42
+ RETURN_VALUE_NOT_JSX: 'BF027',
30
43
 
31
44
  // Component errors (BF043-BF049)
32
45
  PROPS_DESTRUCTURING: 'BF043',
@@ -152,6 +165,9 @@ const errorMessages: Record<ErrorCode, string> = {
152
165
  // stable.
153
166
  'Computed property key in .map() callback destructure is not supported. Rewrite the callback to destructure explicit bindings (e.g., `({ a, b }) => ...`) so the compiler can rewrite references to per-item signal accessors.',
154
167
 
168
+ [ErrorCodes.RETURN_VALUE_NOT_JSX]:
169
+ "Component's return value is not recognized as JSX — return the JSX expression directly instead of binding it to a local variable first.",
170
+
155
171
  [ErrorCodes.PROPS_DESTRUCTURING]:
156
172
  'Props destructuring in function parameters breaks reactivity. Use props object directly.',
157
173
  [ErrorCodes.SIGNAL_GETTER_NOT_CALLED]:
@@ -317,6 +317,32 @@ export type SortComparator = {
317
317
  keys: SortKey[]
318
318
  }
319
319
 
320
+ /**
321
+ * Runtime registries of the sort-comparator catalogue's finite dimensions —
322
+ * the denominators for the coverage ledger's sort floor
323
+ * (`packages/adapter-tests/src/__tests__/coverage-map.test.ts`). This closes
324
+ * the comparator half of the change-time coupling rule
325
+ * (`spec/subset-conformance.md`) mechanically: the exhaustiveness pins below
326
+ * make widening {@link SortKey} without listing the new member here a compile
327
+ * error, and the floor test then makes shipping a listed member with no
328
+ * covering fixture a test failure — same drift defence `PARSED_EXPR_KINDS`
329
+ * and `ARRAY_METHOD_NAMES` provide for their halves.
330
+ */
331
+ export const SORT_KEY_TYPES = ['numeric', 'string', 'auto'] as const satisfies ReadonlyArray<SortKey['type']>
332
+ type MissingFromSortKeyTypes = Exclude<SortKey['type'], (typeof SORT_KEY_TYPES)[number]>
333
+ const _sortKeyTypeRegistryIsExhaustive: MissingFromSortKeyTypes extends never ? true : never = true
334
+ void _sortKeyTypeRegistryIsExhaustive
335
+
336
+ export const SORT_KEY_TARGETS = ['self', 'field'] as const satisfies ReadonlyArray<SortKey['key']['kind']>
337
+ type MissingFromSortKeyTargets = Exclude<SortKey['key']['kind'], (typeof SORT_KEY_TARGETS)[number]>
338
+ const _sortKeyTargetRegistryIsExhaustive: MissingFromSortKeyTargets extends never ? true : never = true
339
+ void _sortKeyTargetRegistryIsExhaustive
340
+
341
+ export const SORT_KEY_DIRECTIONS = ['asc', 'desc'] as const satisfies ReadonlyArray<SortKey['direction']>
342
+ type MissingFromSortKeyDirections = Exclude<SortKey['direction'], (typeof SORT_KEY_DIRECTIONS)[number]>
343
+ const _sortKeyDirectionRegistryIsExhaustive: MissingFromSortKeyDirections extends never ? true : never = true
344
+ void _sortKeyDirectionRegistryIsExhaustive
345
+
320
346
  /**
321
347
  * Flatten depth for `.flat(depth?)` (#1448 Tier C). A finite non-negative
322
348
  * integer flattens that many levels (`.flat()` defaults to `1`; a `0` or
package/src/index.ts CHANGED
@@ -213,7 +213,7 @@ export { isValueReferenceIdentifier, collectValueReferencedNames } from './value
213
213
  // Expression Parser
214
214
  export { parseExpression, tsNodeToParsedExpr, asCallbackMethodCall, CALLBACK_METHODS, sortComparatorFromArrow, serializeParsedExpr, freeVarsInBody, freeIdentifiers, materializeGetterCalls, isSupported, isSupportedValue, exprToString, stringifyParsedExpr, identifierPath, parseBlockBody, parseBlockBodyTolerant, foldBlockToExpr, predicateTernaryToLogical, containsHigherOrder, extractArrowBodyExpression, parseStyleObjectEntries, hasUnsafeStyleValue, parseProviderObjectLiteral, type ProviderObjectMember, type FoldBlockOptions } from './expression-parser.ts'
215
215
  export type { StyleObjectEntry } from './expression-parser.ts'
216
- export { PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES } from './expression-parser.ts'
216
+ export { PARSED_EXPR_KINDS, ARRAY_METHOD_NAMES, SORT_KEY_TYPES, SORT_KEY_TARGETS, SORT_KEY_DIRECTIONS } from './expression-parser.ts'
217
217
  export type { ParsedExpr, ObjectLiteralProperty, ParsedStatement, SortComparator, SortKey, FlatDepth, SupportLevel, SupportResult, TemplatePart } from './expression-parser.ts'
218
218
  export { buildLoopChainExpr } from './loop-chain.ts'
219
219
  export type { LoopChainInputs } from './loop-chain.ts'
@@ -8,7 +8,7 @@ import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from '
8
8
  import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope, anyNameIn } from './reactivity.ts'
9
9
  import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
10
10
  import { detectRootNamespaceWrapTag } from './control-flow/stringify/template-parse.ts'
11
- import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
11
+ import { expandDynamicPropValue, expandConstantForReactivity, resolveRestSpreadOrigin, resolveRestSpreadNames } from './prop-handling.ts'
12
12
  import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
13
13
  import { walkIR, stopAt } from './walker.ts'
14
14
  import { buildLoopChainExpr } from '../loop-chain.ts'
@@ -325,24 +325,47 @@ export function collectInnerLoops(
325
325
  // param) silently dropped its text-child update effect while the
326
326
  // sibling attribute effect (ungated) still fired. Refs need to fire
327
327
  // on every renderItem invocation (#1244).
328
- // - events / conditionals: only in `collectBindings` (branch)
329
- // mode; the legacy non-branch path didn't wire them on
330
- // `NestedLoop` because event delegation handles them through
331
- // the parent's bindings instead.
328
+ // - events: only in `collectBindings` (branch) mode; the
329
+ // legacy non-branch path didn't wire them on `NestedLoop`
330
+ // because event delegation handles them through the parent's
331
+ // bindings instead.
332
+ // - conditionals: collected for EVERY inner loop, branch or not
333
+ // (#2706) — see the `stopAtReactiveConditionals: true` note
334
+ // below for why this must not be gated the same way events are.
332
335
  const bindings: LoopChildBindings = emptyLoopChildBindings()
333
336
  // Hoisted: one Set per loop, not one per child (Copilot review).
334
337
  const innerPreambleNames = preambleNamesOf(n)
335
338
  if (ctx) {
336
339
  for (const child of n.children) {
337
- bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
338
- bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
340
+ // `stopAtReactiveConditionals: true` (#2347's parameter, #2706's
341
+ // fix here) a per-item conditional inside THIS loop's own row
342
+ // now always gets its own `bindings.conditionals` entry (below)
343
+ // and its own `insert()`, regardless of branch/general mode.
344
+ // Descending past it here too (the pre-#2706 default) would
345
+ // double-bind: once via insert()'s bindEvents, once via this
346
+ // flat `insideConditional`-flagged reclaim-on-every-run effect
347
+ // — the latter is also unsound on its own, since nothing
348
+ // guarantees the branch's marker is mounted the moment this
349
+ // effect first runs (issue-2706-nested-loop-conditional-slot
350
+ // .test.ts's original repro).
351
+ bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
352
+ bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, true, innerPreambleNames, n.index))
339
353
  bindings.refs.push(...collectLoopChildRefs(child))
340
354
  }
355
+ bindings.conditionals.push(...collectLoopChildConditionals(
356
+ { type: 'fragment', children: n.children, loc: n.loc } as unknown as IRNode,
357
+ ctx,
358
+ siblingOffsets,
359
+ n.param,
360
+ n.paramBindings,
361
+ innerPreambleNames,
362
+ n.index,
363
+ ))
341
364
  }
342
365
 
343
366
  // Per-item bindings for branch-mode callers (child components,
344
- // events, nested conditionals) — matches the pre-Phase 2
345
- // `collectBranchInnerLoops` behaviour.
367
+ // events) — matches the pre-Phase 2 `collectBranchInnerLoops`
368
+ // behaviour. Conditionals are collected above, uniformly.
346
369
  let childComponents: import('../types.ts').IRLoopChildComponent[] | undefined
347
370
  if (collectBindings) {
348
371
  // skipConditionals=true: components inside conditional branches
@@ -369,18 +392,6 @@ export function collectInnerLoops(
369
392
  for (const child of n.children) {
370
393
  bindings.events.push(...collectLoopChildEventsWithNesting(child))
371
394
  }
372
-
373
- if (ctx) {
374
- bindings.conditionals.push(...collectLoopChildConditionals(
375
- { type: 'fragment', children: n.children, loc: n.loc } as unknown as IRNode,
376
- ctx,
377
- siblingOffsets,
378
- n.param,
379
- n.paramBindings,
380
- innerPreambleNames,
381
- n.index,
382
- ))
383
- }
384
395
  }
385
396
 
386
397
  result.push({
@@ -486,23 +497,15 @@ function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
486
497
  return nodes.length === 1 && nodes[0].type === 'element'
487
498
  }
488
499
 
489
- /** Build rest spread names from context (rest/props spreads handled by applyRestAttrs, not spreadAttrs). */
490
- function buildRestSpreadNames(ctx: ClientJsContext): Set<string> {
491
- const names = new Set<string>()
492
- if (ctx.restPropsName) names.add(ctx.restPropsName)
493
- if (ctx.propsObjectName) names.add(ctx.propsObjectName)
494
- return names
495
- }
496
-
497
500
  /** Build propsExpr for a child component from its IR props. */
498
501
  function buildComponentPropsExpr(props: IRProp[], ctx: ClientJsContext): string {
499
- const restName = ctx.restPropsName
500
- const propsObjName = ctx.propsObjectName
501
502
  const knownSpreadProp = props.find(p => {
502
503
  if (p.name !== '...' && !p.name.startsWith('...')) return false
503
504
  if (p.value.kind !== 'spread' && p.value.kind !== 'expression') return false
504
505
  const expr = p.value.kind === 'spread' ? p.value.expr : p.value.expr
505
- return expr === restName || expr === propsObjName
506
+ // #2723: resolve through any `const x__alias = x` hop onto the
507
+ // rest/props binding — see `resolveRestSpreadOrigin`'s docstring.
508
+ return resolveRestSpreadOrigin(ctx, expr) !== null
506
509
  })
507
510
  const spreadSource = knownSpreadProp ? PROPS_PARAM : null
508
511
 
@@ -751,7 +754,7 @@ export function collectElements(
751
754
  // drops the parent's slot suffix automatically. Each iteration
752
755
  // owns a distinct scope identified by `data-key`, mirroring the
753
756
  // SSR template's renderChild emit.
754
- staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, undefined, undefined)
757
+ staticItemTemplate = irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, undefined, undefined)
755
758
  }
756
759
  } else if (l.children[0] && !projectionInner) {
757
760
  // Pass loopParams so expressions are wrapped at generation time,
@@ -760,8 +763,8 @@ export function collectElements(
760
763
  // in the emitted template literal are rewritten to `__bfItem()[1].color`.
761
764
  const loopParamSpec = [{ param: l.param, bindings: l.paramBindings }]
762
765
  template = useElementReconciliation
763
- ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx), 0, loopParamSpec)
764
- : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, loopParamSpec)
766
+ ? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
767
+ : irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0, loopParamSpec)
765
768
  // Static-array loops emit a `forEach((param, idx) => ...)` whose body
766
769
  // references the destructured param directly — `__bfItem()` is not in
767
770
  // scope there. Build a second template that skips the loop-param
@@ -777,8 +780,8 @@ export function collectElements(
777
780
  // markers, so SSR's parent-anchored shape and CSR's random-id
778
781
  // shape both resolve through the same lookup.
779
782
  staticItemTemplate = useElementReconciliation
780
- ? irToPlaceholderTemplate(l.children[0], buildRestSpreadNames(ctx), 0)
781
- : irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0)
783
+ ? irToPlaceholderTemplate(l.children[0], resolveRestSpreadNames(ctx), 0)
784
+ : irToHtmlTemplate(l.children[0], resolveRestSpreadNames(ctx), 0)
782
785
  } else if (!useElementReconciliation && !l.bodyIsMultiRoot && !l.bodyIsItemConditional) {
783
786
  // Hoisted shared-template fast path (perf): only for the plain
784
787
  // `mapArray` shape — single-root, dynamic array, no element
@@ -844,13 +847,13 @@ export function collectElements(
844
847
  flatMapClient: projectionInner
845
848
  ? {
846
849
  params: l.index ? `(${l.param}, ${l.index})` : `(${l.param})`,
847
- body: renderFlatMapProjectionClientBody(projectionInner, buildRestSpreadNames(ctx)),
850
+ body: renderFlatMapProjectionClientBody(projectionInner, resolveRestSpreadNames(ctx)),
848
851
  keyed: projectionInner.key !== null,
849
852
  }
850
853
  : l.flatMapCallback
851
854
  ? {
852
855
  params: l.flatMapCallback.params,
853
- body: renderFlatMapClientBody(l.flatMapCallback, buildRestSpreadNames(ctx)),
856
+ body: renderFlatMapClientBody(l.flatMapCallback, resolveRestSpreadNames(ctx)),
854
857
  keyed: flatMapCallbackHasKeyedLeaf(l.flatMapCallback),
855
858
  }
856
859
  : undefined,
@@ -953,9 +956,10 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
953
956
  // Always use PROPS_PARAM as the source since the init function parameter is PROPS_PARAM.
954
957
  if (attr.name === '...' && attr.value) {
955
958
  const spreadVal = attrValueToString(attr.value) ?? ''
956
- const elemRestName = ctx.restPropsName
957
- const elemPropsObjName = ctx.propsObjectName
958
- if (spreadVal && (spreadVal === elemRestName || spreadVal === elemPropsObjName)) {
959
+ // #2723: resolve through any `const x__alias = x` hop onto the
960
+ // rest/props binding — see `resolveRestSpreadOrigin`'s docstring.
961
+ const spreadOrigin = spreadVal ? resolveRestSpreadOrigin(ctx, spreadVal) : null
962
+ if (spreadOrigin !== null) {
959
963
  // `applyRestAttrs(_el, _p, exclude)` is handed the FULL props
960
964
  // object (`PROPS_PARAM`), not a computed JS rest binding, and the
961
965
  // runtime filters by SOURCE KEY (`source[key]`). So `exclude` must
@@ -974,7 +978,7 @@ function collectFromElement(element: IRElement, ctx: ClientJsContext, insideCond
974
978
  // caller-keyed (#2524 CSR half) — so the exclude list must use
975
979
  // the caller-facing key too, not the local binding name.
976
980
  const consumedKeys =
977
- spreadVal === elemRestName ? ctx.propsParams.map(p => p.sourceName ?? p.name) : []
981
+ spreadOrigin === 'rest' ? ctx.propsParams.map(p => p.sourceName ?? p.name) : []
978
982
  const staticAttrKeys = element.attrs
979
983
  .filter(a => a.name !== '...')
980
984
  .map(a => a.name)
@@ -1124,7 +1128,7 @@ function collectBranchLoops(
1124
1128
  siblingOffsets: Map<IRLoop, IRNode[]>,
1125
1129
  ): BranchLoop[] {
1126
1130
  const loops: BranchLoop[] = []
1127
- const restNames = ctx ? buildRestSpreadNames(ctx) : undefined
1131
+ const restNames = ctx ? resolveRestSpreadNames(ctx) : undefined
1128
1132
 
1129
1133
  walkIR<string | null>(node, null, {
1130
1134
  // Don't recurse into nested conditionals / if-statements.
@@ -1247,7 +1251,7 @@ function buildConditionalMetadata(
1247
1251
  ctx: ClientJsContext,
1248
1252
  siblingOffsets: Map<IRLoop, IRNode[]>,
1249
1253
  ): ConditionalElement {
1250
- const restNames = buildRestSpreadNames(ctx)
1254
+ const restNames = resolveRestSpreadNames(ctx)
1251
1255
  // Use loopDepth=-1 so the first loop encountered inside the branch emits
1252
1256
  // data-key (depth 0) for its items, matching the mapArray item template
1253
1257
  // and event dispatcher convention. Matches irToComponentTemplate/generateCsrTemplate.
@@ -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[] = []