@barefootjs/jsx 0.33.3 → 0.33.6

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 (87) hide show
  1. package/dist/adapters/child-scope.d.ts +13 -0
  2. package/dist/adapters/child-scope.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/errors.d.ts +1 -0
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1392 -1117
  9. package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
  11. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/component-scope.d.ts +20 -0
  13. package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/control-flow/plan/build-component-loop.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +2 -4
  17. package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -3
  19. package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/control-flow/stringify/component-loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
  22. package/dist/ir-to-client-js/control-flow.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
  24. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
  26. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/html-template.d.ts +49 -2
  28. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/imports.d.ts +2 -2
  30. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  31. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  32. package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
  33. package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
  34. package/dist/jsx-to-ir.d.ts.map +1 -1
  35. package/dist/prop-rewrite.d.ts +5 -2
  36. package/dist/prop-rewrite.d.ts.map +1 -1
  37. package/dist/props-binding.d.ts +49 -11
  38. package/dist/props-binding.d.ts.map +1 -1
  39. package/dist/types.d.ts +19 -5
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
  43. package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
  44. package/src/__tests__/date-lowering.test.ts +18 -4
  45. package/src/__tests__/fragment-body-loop-key.test.ts +95 -0
  46. package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
  47. package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
  48. package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
  49. package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
  50. package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
  51. package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
  52. package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
  53. package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
  54. package/src/__tests__/loop-item-root-scope.test.ts +7 -3
  55. package/src/__tests__/markup-prop-brand.test.ts +165 -26
  56. package/src/__tests__/multi-root-loop-body.test.ts +7 -3
  57. package/src/__tests__/namespace-import-primitive.test.ts +244 -0
  58. package/src/__tests__/preamble-declarations.test.ts +42 -0
  59. package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
  60. package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
  61. package/src/adapters/child-scope.ts +23 -0
  62. package/src/analyzer.ts +130 -0
  63. package/src/errors.ts +14 -1
  64. package/src/index.ts +7 -0
  65. package/src/ir-to-client-js/build-references.ts +7 -0
  66. package/src/ir-to-client-js/child-components.ts +8 -1
  67. package/src/ir-to-client-js/collect-elements.ts +10 -38
  68. package/src/ir-to-client-js/component-scope.ts +60 -0
  69. package/src/ir-to-client-js/compute-inlinability.ts +46 -11
  70. package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +19 -1
  71. package/src/ir-to-client-js/control-flow/plan/loop.ts +2 -4
  72. package/src/ir-to-client-js/control-flow/shared.ts +12 -5
  73. package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +7 -0
  74. package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
  75. package/src/ir-to-client-js/control-flow.ts +12 -0
  76. package/src/ir-to-client-js/csr-substitute.ts +82 -16
  77. package/src/ir-to-client-js/emit-reactive.ts +29 -13
  78. package/src/ir-to-client-js/emit-registration.ts +1 -1
  79. package/src/ir-to-client-js/html-template.ts +194 -82
  80. package/src/ir-to-client-js/imports.ts +4 -0
  81. package/src/ir-to-client-js/index.ts +34 -23
  82. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
  83. package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
  84. package/src/jsx-to-ir.ts +318 -54
  85. package/src/prop-rewrite.ts +23 -5
  86. package/src/props-binding.ts +74 -18
  87. package/src/types.ts +19 -5
@@ -15,6 +15,7 @@ import { datePlugin, DATE_METHODS } from '../date-lowering.ts'
15
15
  import { toLocaleDatePlugin, foldedArgToClientJs } from '../to-locale-date-lowering.ts'
16
16
  import { tsNodeToParsedExpr } from '../expression-parser.ts'
17
17
  import type { LoweringMatcher } from '../lowering-registry.ts'
18
+ import { rewriteBarePropRefs } from '../prop-rewrite.ts'
18
19
 
19
20
  /**
20
21
  * Profile mode (#1690, SR3/SR4): the id appended to a DOM-binding effect so the
@@ -127,28 +128,43 @@ export function emitAttrUpdate(target: string, attrName: string, expression: str
127
128
  /**
128
129
  * Rewrite destructured prop names in an expression to `(props.xxx ?? default)`.
129
130
  * Only applies when the component uses destructured props (not props.xxx style).
131
+ *
132
+ * Delegates the actual walk to `rewriteBarePropRefs` (`prop-rewrite.ts`) — the
133
+ * one AST walk in the compiler that already distinguishes a VALUE reference
134
+ * from an object-literal key, a property-access name, a shorthand property,
135
+ * or a name shadowed by an inner binding (`items.map((tag) => tag.x)`).
136
+ * This function used to run its own `\btag\b`-style regex over the raw
137
+ * expression text with no such distinction, so `queryHref(base, { tag: tag })`
138
+ * rewrote the KEY too (`{ _p.tag: _p.tag }`, a syntax error) — #2741. Only
139
+ * the default-value `(_p.x ?? <default>)` wrapping is specific to this call
140
+ * site, supplied via `replacementFor` rather than a second copy of the walk.
130
141
  */
131
142
  export function rewriteDestructuredPropsInExpr(expr: string, ctx: ClientJsContext): string {
132
143
  if (ctx.propsObjectName) return expr
133
144
 
134
- const { protect, restore } = createTemplateAwareStringProtector()
135
- let result = protect(expr)
136
-
145
+ const propNames = new Set<string>()
146
+ const propAliases = new Map<string, string>()
147
+ const propsByName = new Map<string, ClientJsContext['propsParams'][number]>()
137
148
  for (const prop of ctx.propsParams) {
138
149
  if (prop.name === 'children') continue
139
- const pattern = new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g')
140
- if (!pattern.test(result)) continue
141
-
150
+ propNames.add(prop.name)
151
+ propsByName.set(prop.name, prop)
142
152
  // `_p` is always keyed by the caller-facing name (#2524 CSR half).
143
- const callerKey = prop.sourceName ?? prop.name
144
- const defaultVal = prop.defaultValue
145
- const replacement = defaultVal
146
- ? `(${PROPS_PARAM}.${callerKey} ?? ${prop.defaultContainsArrow ? `(${defaultVal})` : defaultVal})`
147
- : `${PROPS_PARAM}.${callerKey}`
148
- result = result.replace(new RegExp(`(?<![-.])\\b${prop.name}\\b`, 'g'), replacement)
153
+ if (prop.sourceName && prop.sourceName !== prop.name) propAliases.set(prop.name, prop.sourceName)
149
154
  }
155
+ if (propNames.size === 0) return expr
150
156
 
151
- return restore(result)
157
+ const replacementFor = (localName: string, callerKey: string): string => {
158
+ const defaultVal = propsByName.get(localName)?.defaultValue
159
+ if (!defaultVal) return `${PROPS_PARAM}.${callerKey}`
160
+ const defaultContainsArrow = propsByName.get(localName)?.defaultContainsArrow
161
+ return `(${PROPS_PARAM}.${callerKey} ?? ${defaultContainsArrow ? `(${defaultVal})` : defaultVal})`
162
+ }
163
+
164
+ // Parenthesized so an object-literal or arrow-function expression parses
165
+ // standalone — same convention `applyScopedPropRefRewrite` itself uses.
166
+ const sourceFile = ts.createSourceFile('__bf_reactive_expr.ts', `(${expr}\n)`, ts.ScriptTarget.Latest, true)
167
+ return rewriteBarePropRefs(expr, sourceFile, propNames, undefined, propAliases, replacementFor) ?? expr
152
168
  }
153
169
 
154
170
  /**
@@ -223,7 +223,7 @@ export function emitRegistrationAndHydration(
223
223
  // (#2651) — see `generateCsrTemplate`'s identical derivation below
224
224
  // for why this is reused as-is rather than re-derived.
225
225
  const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
226
- const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
226
+ const templateHtml = irToComponentTemplate(_ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds, ctx.restPropsName)
227
227
  if (templateHtml) {
228
228
  defParts.push(buildTemplateDefPart(ctx, templateHtml))
229
229
  }
@@ -2,17 +2,18 @@
2
2
  * IR → HTML template string generation and validation.
3
3
  */
4
4
 
5
- import type { AttrValue, FlatMapCallback, IRAttribute, IRExpression, IRNode, IRProp, MapCallbackPreamble } from '../types.ts'
5
+ import type { AttrValue, FlatMapCallback, IRAttribute, IRElement, IRExpression, IRNode, IRProp, MapCallbackPreamble } from '../types.ts'
6
6
  import { isBooleanAttr } from '../html-constants.ts'
7
7
  import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_PH, keyAttrName, loopStartMarker, loopEndMarker, loopItemMarker, freeIdsFromRefs, setIntersects, wrapExprWithLoopParams } from './utils.ts'
8
8
  import type { LoopParamSpec } from './utils.ts'
9
9
  import { nameForRegistryRef } from './component-scope.ts'
10
10
  import { assertNever } from './walker.ts'
11
- import { buildSignalMemoEnv, csrSubstitute, applyPropsRewrite, type CsrEnv } from './csr-substitute.ts'
11
+ import { buildSignalMemoEnv, csrSubstitute, type CsrEnv } from './csr-substitute.ts'
12
+ import { rewritePropsObjectRef } from './rewrite-props-object.ts'
12
13
  import type { ClientJsContext } from './types.ts'
13
- import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
14
+ import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, classifyDOMProp, escapeHtml } from '@barefootjs/shared'
14
15
  import { buildLoopChainExpr } from '../loop-chain.ts'
15
- import { derivesScopeFromSlot } from '../adapters/child-scope.ts'
16
+ import { renderChildScopeArgs } from '../adapters/child-scope.ts'
16
17
  import { BindingScope } from '../scope/binding-scope.ts'
17
18
 
18
19
  /**
@@ -547,6 +548,25 @@ export interface MergeContext {
547
548
  honorClientOnly: boolean
548
549
  }
549
550
 
551
+ /**
552
+ * The client-template twin of `IRElement.keyAttr`'s SSR-side decision
553
+ * (`jsx-to-ir.ts`'s `extractLoopKey`/`applyLoopKeyAttr`) — #2763. A raw
554
+ * `key={}` JSX attribute survives on `.attrs` regardless of whether the
555
+ * loop/relay actually recognized it as a row key (a fragment-bodied loop
556
+ * row before #2763's fix, or heterogeneous keys across conditional
557
+ * branches) — baking `data-key` from the mere presence of that attribute
558
+ * would disagree with every SSR adapter, which all gate on `keyAttr`
559
+ * instead. Returns the resolved attribute name only when `node.keyAttr` is
560
+ * actually set; `null` otherwise, so the caller drops the raw `key`
561
+ * attribute entirely rather than emitting a `data-key` no SSR adapter
562
+ * agrees with. `keyAttr.name` and `keyAttrName(loopDepth)` are already the
563
+ * same value (both derive from `@barefootjs/shared`'s `keyAttrName`, see
564
+ * `IRElement.keyAttr`'s docstring) — this only decides WHETHER to use it.
565
+ */
566
+ function resolvedKeyAttrName(node: IRElement, loopDepth: number): string | null {
567
+ return node.keyAttr ? keyAttrName(loopDepth) : null
568
+ }
569
+
550
570
  /** Return true if this attribute should participate in the merge object. */
551
571
  function isMergeableAttr(a: IRAttribute, ctx: MergeContext): boolean {
552
572
  if (ctx.honorClientOnly && a.clientOnly) return false
@@ -639,10 +659,11 @@ function buildSpreadAttrsMergeCall(args: {
639
659
  * are wrapped with `__bfSlot(EXPR, <branchSlotsVar>)` so the runtime can
640
660
  * splice live `Node` returns into the parsed fragment instead of
641
661
  * stringifying them via the template literal (#1213).
642
- * Component nodes omit their `slotId` from the `renderChild` call exactly
643
- * when `derivesScopeFromSlot()` says so — a loop-item-root component (its
644
- * own scope, identified by `data-key`) never derives from the parent slot;
645
- * a component nested below the row root does (#2444). See
662
+ * Component nodes always pass their `slotId` to `renderChild` (via
663
+ * `renderChildScopeArgs`) — a loop-item-root component (its own scope,
664
+ * identified by `data-key`) keeps `bf-h`/`bf-m` slot identity without
665
+ * DERIVING its `bf-s` scope id from the parent slot (#2833); a component
666
+ * nested below the row root derives from it as usual (#2444). See
646
667
  * `IRComponent.loopItemRoot` / `./adapters/child-scope.ts`.
647
668
  */
648
669
  /**
@@ -876,10 +897,11 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
876
897
  // others are already represented inside the merge object.
877
898
  return idx === firstMergeableIdx ? mergeCall! : ''
878
899
  }
879
- const attrName = a.name === '...'
880
- ? '...'
881
- : (a.name === 'key' ? keyAttrName(loopDepth) : toHtmlAttrName(a.name))
882
- return renderTemplateAttrPart(a, attrName, wrapExpr, restSpreadNames)
900
+ if (a.name === 'key') {
901
+ const attrName = resolvedKeyAttrName(node, loopDepth)
902
+ return attrName ? renderTemplateAttrPart(a, attrName, wrapExpr, restSpreadNames) : ''
903
+ }
904
+ return renderTemplateAttrPart(a, toHtmlAttrName(a.name), wrapExpr, restSpreadNames)
883
905
  })
884
906
  .filter(Boolean)
885
907
 
@@ -1059,12 +1081,11 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
1059
1081
  const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? 'undefined'}` : ''
1060
1082
  // Pass slotId as suffix so $c() can find the child component by slot
1061
1083
  // after branch swap. A loop-item-root component owns a separate scope
1062
- // per iteration (identified by `data-key`), so the parent-slot suffix
1063
- // is dropped to avoid anchoring it to a wrong component-wrapper scope
1064
- // (#1268) a component nested BELOW the row root still derives from
1065
- // its slot, matching the Hono reference (#2444).
1066
- const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
1067
- return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
1084
+ // per iteration (identified by `data-key`) rather than deriving from
1085
+ // the slot `renderChildScopeArgs` still passes its slotId with the
1086
+ // `loopItemRoot` flag (#2833) so it keeps `bf-h`/`bf-m` slot identity,
1087
+ // matching the Hono reference (#2444).
1088
+ return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${renderChildScopeArgs(node, keyArg)})}`
1068
1089
  }
1069
1090
 
1070
1091
  case 'loop': {
@@ -1189,7 +1210,9 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
1189
1210
  if (a.name === '...') return null
1190
1211
  if (a.name === 'dangerouslySetInnerHTML') return null
1191
1212
  if (a.name === 'key') {
1192
- attrParts.push(`${keyAttrName(0)}=""`)
1213
+ if (resolvedKeyAttrName(node, 0)) {
1214
+ attrParts.push(`${keyAttrName(0)}=""`)
1215
+ }
1193
1216
  continue
1194
1217
  }
1195
1218
  const v = a.value
@@ -1208,6 +1231,21 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
1208
1231
  case 'template': {
1209
1232
  const attrKey = node.slotId ? `${node.slotId}::${a.name}` : null
1210
1233
  if (!attrKey || !safe.reactiveAttrKeys.has(attrKey)) return null
1234
+ // #2756 (input sub-mechanism): "omit and let the effect's eager
1235
+ // first run fill it in" only holds for attribute-reflected
1236
+ // binds — the effect calls `setAttribute`, so a freshly-cloned
1237
+ // row ends up byte-identical to an SSR-baked/hydrated one. A
1238
+ // property-only bind (`value`/`checked`, `classifyDOMProp`)
1239
+ // never does: `emitAttrUpdate`'s value/boolean-attr branches
1240
+ // deliberately write only the DOM PROPERTY (#2716), never the
1241
+ // attribute. SSR still bakes the attribute (`clientOnly` is
1242
+ // false here — `clientOnly` binds, e.g. textarea/select's
1243
+ // `value`, take neither path since SSR omits their attribute
1244
+ // too, so omitting stays correct for them). Refuse the whole
1245
+ // skeleton fast path instead of just this attribute; the
1246
+ // caller falls back to `irToHtmlTemplate`, which already bakes
1247
+ // this attribute correctly.
1248
+ if (!a.clientOnly && classifyDOMProp(a.name).kind === 'property') return null
1211
1249
  // Covered by a loop-child createEffect — omit from the skeleton
1212
1250
  // entirely; the effect's eager first run fills it in.
1213
1251
  break
@@ -1485,9 +1523,11 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Readonly
1485
1523
  // composite-row twin of it, so a row it builds must carry the
1486
1524
  // same attributes a hydration-reused row does (#2756).
1487
1525
  if (a.clientOnly) return ''
1488
- const attrName = a.name === '...'
1489
- ? '...'
1490
- : (a.name === 'key' ? keyAttrName(loopDepth) : toHtmlAttrName(a.name))
1526
+ if (a.name === 'key') {
1527
+ const attrName = resolvedKeyAttrName(node, loopDepth)
1528
+ return attrName ? renderTemplateAttrPart(a, attrName, wrapExpr, restSpreadNames) : ''
1529
+ }
1530
+ const attrName = a.name === '...' ? '...' : toHtmlAttrName(a.name)
1491
1531
  return renderTemplateAttrPart(a, attrName, wrapExpr, restSpreadNames)
1492
1532
  })
1493
1533
  .filter(Boolean)
@@ -1604,34 +1644,52 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Readonly
1604
1644
  * - IRConditional → ternary expression
1605
1645
  * - Single child → single expression; multiple → array literal
1606
1646
  */
1607
- export function irChildrenToJsExpr(children: IRNode[]): string {
1647
+ export function irChildrenToJsExpr(children: IRNode[], brandMarkup = false): string {
1608
1648
  // Flatten fragments and filter empty text nodes
1609
- const exprs = children.flatMap(c => irNodeToJsExprs(c)).filter(Boolean)
1649
+ const exprs = children.flatMap(c => irNodeToJsExprs(c, brandMarkup)).filter(Boolean)
1610
1650
  if (exprs.length === 0) return "''"
1611
1651
  if (exprs.length === 1) return exprs[0]
1612
1652
  return `[${exprs.join(', ')}]`
1613
1653
  }
1614
1654
 
1615
1655
  /**
1616
- * Narrow gate for branding a nested `jsx-children` getter's value with
1617
- * `bfMarkup()` (#2651) — the `irNodeToJsExprs` counterpart of
1618
- * `collect-elements.ts`'s `isSingleElementJsxChildren` (duplicated rather
1619
- * than imported: `collect-elements.ts` already imports FROM this file, so
1620
- * the reverse import would cycle). Same shape, same reasoning: a
1621
- * `jsx-children` prop's value is always a single stored node
1622
- * (`AttrValueOf.jsxChildren([...])`); only when that node is a lone
1623
- * `'element'` does `irChildrenToJsExpr` reduce it to ONE HTML-string
1624
- * template literal the shape `bfMarkup` + `escapeTextOrNode` /
1625
- * `escapeTextOrMarkup` have a proven contract for. A `'fragment'` (multiple
1626
- * children) or `'conditional'` node reduces to an array literal or a
1627
- * nested ternary instead — left unbranded here for the same reason as the
1628
- * `collect-elements.ts` twin.
1656
+ * Build the getter-body expression for a `jsx-children` prop value
1657
+ * `collect-elements.ts`'s `buildComponentPropsExpr` and this file's
1658
+ * `irNodeToJsExprs` 'component' case both hand a child component this exact
1659
+ * value through `get <propName>() { return <expr> }`. The ONE place that
1660
+ * decides whether (and how) to brand it with `bfMarkup()` (#2651, #2702).
1661
+ *
1662
+ * `children` itself is never branded — its consumer is a bare
1663
+ * `{children}` passthrough interpolation with no unwrap call, so a branded
1664
+ * object there would stringify to `[object Object]`.
1665
+ *
1666
+ * Otherwise, brand every `'element'` LEAF (`irNodeToJsExprs(..., true)`),
1667
+ * not the whole assembled value: `escapeTextOrNode`/`escapeTextOrMarkup`
1668
+ * (`@barefootjs/client/runtime`) unwrap a `bfMarkup()`-branded STRING, so a
1669
+ * lone element (`header={<strong/>}`), a fragment wrapping one (`header={<>
1670
+ * <strong/></>}`), and a conditional/nested-conditional between elements
1671
+ * (`header={<>{cond ? <a/> : <b/>}</>}`, #2702) all reduce, after flattening,
1672
+ * to exactly ONE branded expression — brand-at-the-leaf makes this sound
1673
+ * even when a branch is plain text or an arbitrary expression
1674
+ * (`cond ? <a/> : label()`): `irNodeToJsExprs`'s 'text'/'expression' cases
1675
+ * never brand, so `label()` still reaches `escapeTextOrNode` unescaped-HTML-free
1676
+ * and gets escaped normally, while the `<a/>` branch is trusted.
1677
+ *
1678
+ * A multi-child fragment (`header={<>text<strong/></>}`) flattens to MORE
1679
+ * than one expression — an array, per `irChildrenToJsExpr`'s own contract —
1680
+ * and neither runtime unwrapper has an array contract, so that shape stays
1681
+ * exactly as it rendered before this function existed: unbranded, via the
1682
+ * plain (unbranded) `irChildrenToJsExpr` call below. This is a pre-existing,
1683
+ * separate gap (tracked apart from #2702), not something this function
1684
+ * silently "fixes" by guessing at a new consumer contract.
1629
1685
  */
1630
- function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
1631
- return nodes.length === 1 && nodes[0].type === 'element'
1686
+ export function jsxChildrenPropGetterExpr(children: IRNode[], propName: string): string {
1687
+ if (propName === 'children') return irChildrenToJsExpr(children)
1688
+ const brandedExprs = children.flatMap(c => irNodeToJsExprs(c, true)).filter(Boolean)
1689
+ return brandedExprs.length === 1 ? brandedExprs[0] : irChildrenToJsExpr(children)
1632
1690
  }
1633
1691
 
1634
- function irNodeToJsExprs(node: IRNode): string[] {
1692
+ function irNodeToJsExprs(node: IRNode, brandMarkup = false): string[] {
1635
1693
  switch (node.type) {
1636
1694
  case 'component': {
1637
1695
  const propsEntries: string[] = node.props
@@ -1642,17 +1700,10 @@ function irNodeToJsExprs(node: IRNode): string[] {
1642
1700
  }
1643
1701
  switch (p.value.kind) {
1644
1702
  case 'jsx-children': {
1645
- // Brand (#2651) only the lone-'element' shape, and never an
1646
- // explicit `children={<jsx/>}` prop — see
1647
- // `isSingleElementJsxChildren` (this file) for the shape
1648
- // argument and the `collect-elements.ts` twin for the
1649
- // `children`-exclusion argument (bare-passthrough consumer,
1650
- // no unwrap call).
1651
- const jsxExpr = irChildrenToJsExpr(p.value.children)
1652
- const wrapped = (p.name !== 'children' && isSingleElementJsxChildren(p.value.children))
1653
- ? `bfMarkup(${jsxExpr})`
1654
- : jsxExpr
1655
- return `get ${quotePropName(p.name)}() { return ${wrapped} }`
1703
+ // Brand (#2651/#2702) via the shared door see
1704
+ // `jsxChildrenPropGetterExpr`'s docstring.
1705
+ const jsxExpr = jsxChildrenPropGetterExpr(p.value.children, p.name)
1706
+ return `get ${quotePropName(p.name)}() { return ${jsxExpr} }`
1656
1707
  }
1657
1708
  case 'literal':
1658
1709
  return `get ${quotePropName(p.name)}() { return ${JSON.stringify(p.value.value)} }`
@@ -1683,14 +1734,16 @@ function irNodeToJsExprs(node: IRNode): string[] {
1683
1734
  if (!node.value.trim()) return []
1684
1735
  return [JSON.stringify(node.value)]
1685
1736
 
1686
- case 'element':
1687
- return [`\`${irToHtmlTemplate(node)}\``]
1737
+ case 'element': {
1738
+ const html = `\`${irToHtmlTemplate(node)}\``
1739
+ return [brandMarkup ? `bfMarkup(${html})` : html]
1740
+ }
1688
1741
 
1689
1742
  case 'fragment':
1690
- return node.children.flatMap(c => irNodeToJsExprs(c))
1743
+ return node.children.flatMap(c => irNodeToJsExprs(c, brandMarkup))
1691
1744
 
1692
1745
  case 'conditional':
1693
- return [`${node.condition} ? ${irChildrenToJsExpr([node.whenTrue])} : ${irChildrenToJsExpr([node.whenFalse])}`]
1746
+ return [`${node.condition} ? ${irChildrenToJsExpr([node.whenTrue], brandMarkup)} : ${irChildrenToJsExpr([node.whenFalse], brandMarkup)}`]
1694
1747
 
1695
1748
  // The kinds below previously fell through to `default: return []` and so
1696
1749
  // were silently dropped when used as a component child. The explicit
@@ -1747,6 +1800,21 @@ export interface TemplateOptions {
1747
1800
  inlinableConstants?: Map<string, string>
1748
1801
  restSpreadNames?: ReadonlySet<string>
1749
1802
  propsObjectName?: string | null
1803
+ /**
1804
+ * The rest-bag binding's source name (`ctx.restPropsName` — `'rest'` in
1805
+ * `{ children, ...rest }`), threaded alongside `propsObjectName` into
1806
+ * `rewritePropsObjectRef` so a component that reads the rest binding
1807
+ * directly in its own body (`{rest.header}`, not just spread onto an
1808
+ * element via `applyRestAttrs`) resolves to `_p` here too (#2806).
1809
+ * Not folded into `restSpreadNames`: that set is spread-filter
1810
+ * membership keyed by raw expression text (`isFilteredSpread`,
1811
+ * `collect-elements.ts`'s exclude-key resolution) and must keep seeing
1812
+ * the untouched name; this field is the separate "identifier to rewrite"
1813
+ * input `rewritePropsObjectRef` already accepts for the init body
1814
+ * (`generate-init.ts`) — the four template builders below were the only
1815
+ * callers still passing `null` for it.
1816
+ */
1817
+ restPropsName?: string | null
1750
1818
  /**
1751
1819
  * Names that exist only in the init-body scope (or were demoted to unsafe
1752
1820
  * during chained-ref resolution). The CSR template runs at module scope
@@ -1838,13 +1906,14 @@ export function irToComponentTemplate(
1838
1906
  inlinableConstants?: Map<string, string>,
1839
1907
  restSpreadNames?: ReadonlySet<string>,
1840
1908
  propsObjectName?: string | null,
1841
- markupSlotIds?: ReadonlySet<string>
1909
+ markupSlotIds?: ReadonlySet<string>,
1910
+ restPropsName?: string | null
1842
1911
  ): string {
1843
- return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1, markupSlotIds })
1912
+ return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1, markupSlotIds, restPropsName })
1844
1913
  }
1845
1914
 
1846
1915
  function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
1847
- const { inlinableConstants, restSpreadNames, propsObjectName, loopDepth = 0 } = opts
1916
+ const { inlinableConstants, restSpreadNames, propsObjectName, restPropsName, loopDepth = 0 } = opts
1848
1917
  // `recurse` preserves `inHoistedChildren` for structural pass-through
1849
1918
  // (fragment / conditional); `childrenRecurse` clears it so element
1850
1919
  // descendants don't re-emit the placeholder. (#1320)
@@ -1865,15 +1934,31 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
1865
1934
  }
1866
1935
  }
1867
1936
 
1868
- // Normalize source-level props object access (e.g., props.xxx → _p.xxx)
1869
- if (propsObjectName && propsObjectName !== PROPS_PARAM) {
1870
- result = result.replace(
1871
- new RegExp(`\\b${propsObjectName}\\.`, 'g'),
1872
- `${PROPS_PARAM}.`,
1873
- )
1874
- }
1875
-
1876
- return restore(result)
1937
+ // Normalize source-level props object access (e.g., props.xxx → _p.xxx).
1938
+ // Restore FIRST: `rewritePropsObjectRef` parses `result` as real
1939
+ // TypeScript, and a still-protected string/template-literal span would
1940
+ // parse as an opaque placeholder token instead of the source text it
1941
+ // stands for (harmless for the placeholder itself, but it must not be
1942
+ // mistaken for unparsed leftover input by a caller reading `result`
1943
+ // before this point). Regex-based `\b${propsObjectName}\.` (removed
1944
+ // here) required the name to be immediately followed by `.`, so a
1945
+ // parenthesised receiver — `(props).label`, produced by this same
1946
+ // function's own constant-inlining step just above when the inlined
1947
+ // value is itself `props` (a local alias of the whole props object) —
1948
+ // silently kept the pre-rewrite name instead of `_p`.
1949
+ //
1950
+ // The gate stays keyed on `propsObjectName` for the destructured-prop
1951
+ // case (unconditionally rewriting here would also change static-path
1952
+ // output for components with neither a props object nor a rest binding
1953
+ // — a separate alignment, not this fix) but widens to fire when only
1954
+ // a rest binding exists (`{ children, ...rest }` with no whole-props
1955
+ // param), so `rest.header` resolves to `_p.header` here the same way
1956
+ // it already does in the init body (`generate-init.ts`, #2723) instead
1957
+ // of reaching the template as a bare, undeclared `rest` (#2806).
1958
+ const restored = restore(result)
1959
+ return (propsObjectName && propsObjectName !== PROPS_PARAM) || restPropsName
1960
+ ? rewritePropsObjectRef(restored, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: opts.scope })
1961
+ : restored
1877
1962
  }
1878
1963
 
1879
1964
  switch (node.type) {
@@ -2250,8 +2335,11 @@ export function generateCsrTemplate(
2250
2335
  // Build the substitution env once per component. Signals + memos come
2251
2336
  // from `buildSignalMemoEnv`; inlinable constants layer in here so
2252
2337
  // each call to `transformExpr` is a pure AST projection over the
2253
- // shared env (no per-call Map cloning).
2254
- const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null)
2338
+ // shared env (no per-call Map cloning). `ctx.localConstants` lets
2339
+ // `buildSignalMemoEnv` also resolve a bare getter-alias chain
2340
+ // (`const items__alias = items`, #2778) to the same call-kind entry
2341
+ // its origin has.
2342
+ const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null, ctx.localConstants)
2255
2343
  const csrEnv: CsrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName }
2256
2344
  if (inlinableConstants) {
2257
2345
  for (const [name, value] of inlinableConstants) {
@@ -2268,7 +2356,7 @@ export function generateCsrTemplate(
2268
2356
  // by construction (`collectElements`'s `expression` visitor only pushes
2269
2357
  // here). Reused as-is, not re-derived.
2270
2358
  const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
2271
- return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1, markupSlotIds })
2359
+ return generateCsrTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames: effectiveUnsafeLocalNames, deferredChildSlots, loopDepth: -1, markupSlotIds, restPropsName: ctx.restPropsName })
2272
2360
  }
2273
2361
 
2274
2362
  /**
@@ -2345,7 +2433,7 @@ function buildCsrEnvForCtx(
2345
2433
  inlinableConstants: Map<string, string> | undefined,
2346
2434
  propsObjectName?: string | null,
2347
2435
  ): CsrEnv {
2348
- const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null)
2436
+ const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null, ctx.localConstants)
2349
2437
  const csrEnv: CsrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName }
2350
2438
  if (inlinableConstants) {
2351
2439
  for (const [name, value] of inlinableConstants) {
@@ -2463,7 +2551,7 @@ export function computeDeferredChildSlots(
2463
2551
  }
2464
2552
 
2465
2553
  function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
2466
- const { restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
2554
+ const { restSpreadNames, propsObjectName, restPropsName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
2467
2555
  const env: CsrEnv = csrEnv ?? { substitutions: new Map(), propsObjectName: propsObjectName ?? null }
2468
2556
  const transformExpr = (expr: string, templateExpr?: string): string => {
2469
2557
  // Single AST substitution pass: replaces signal getter calls
@@ -2492,11 +2580,16 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2492
2580
  if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(freeIdentifiers, unsafeLocalNames)) {
2493
2581
  return UNSAFE_TEMPLATE_EXPR
2494
2582
  }
2495
- // Final emit-form: rewrite `propsName.X → _p.X`. Deferred until
2496
- // after the unsafe-name check because the check used raw form
2583
+ // Final emit-form: rewrite `propsName.X → _p.X` (and a direct
2584
+ // rest-bag read, `restName.X _p.X` the rest binding IS `_p` at
2585
+ // runtime, same as the init body's `rewritePropsObjectRef` call and
2586
+ // `applyRestAttrs`'s exclude-key filtering both already assume; a
2587
+ // computed "props minus consumed keys" object here would be a second,
2588
+ // divergent implementation of that same decision, #2806). Deferred
2589
+ // until after the unsafe-name check because the check used raw form
2497
2590
  // (consistent with `populateCsrInlinable`'s `isInlinableInTemplate`
2498
2591
  // gate — #1138).
2499
- return applyPropsRewrite(rewritten, propsObjectName ?? null)
2592
+ return rewritePropsObjectRef(rewritten, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: opts.scope })
2500
2593
  }
2501
2594
 
2502
2595
  // `recurse` preserves `inHoistedChildren` for structural pass-through
@@ -2550,9 +2643,26 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2550
2643
  if (mergeCtx.isFilteredSpread(v)) return ''
2551
2644
  return `\${spreadAttrs(${transformExpr(v.expr, v.templateExpr)})}`
2552
2645
  }
2553
- const attrName = a.name === 'key'
2554
- ? keyAttrName(loopDepth)
2555
- : toHtmlAttrName(a.name)
2646
+ if (a.name === 'key') {
2647
+ const keyName = resolvedKeyAttrName(node, loopDepth)
2648
+ if (!keyName) return ''
2649
+ switch (v.kind) {
2650
+ case 'boolean-attr':
2651
+ return keyName
2652
+ case 'literal':
2653
+ return `${keyName}="${escapeHtml(v.value)}"`
2654
+ case 'expression':
2655
+ return templateAttrExpr(keyName, transformExpr(v.expr, v.templateExpr), v.presenceOrUndefined)
2656
+ case 'template': {
2657
+ const valueStr = attrValueToString(v, { useTemplate: true })
2658
+ return valueStr ? templateAttrExpr(keyName, transformExpr(valueStr)) : ''
2659
+ }
2660
+ case 'boolean-shorthand':
2661
+ case 'jsx-children':
2662
+ return ''
2663
+ }
2664
+ }
2665
+ const attrName = toHtmlAttrName(a.name)
2556
2666
  switch (v.kind) {
2557
2667
  case 'boolean-attr':
2558
2668
  return attrName
@@ -2731,8 +2841,10 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2731
2841
  const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(', ')}}` : '{}'
2732
2842
  const keyProp = node.props.find(p => p.name === 'key')
2733
2843
  const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : ''
2734
- const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
2735
- return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${keyArg || (slotArg ? ', undefined' : '')}${slotArg})}`
2844
+ // See `renderChildScopeArgs`'s docstring a loop item root still gets
2845
+ // its slotId, plus the `loopItemRoot` flag (#2833), rather than no
2846
+ // slot argument at all.
2847
+ return `\${renderChild('${nameForRegistryRef(node.name)}', ${propsExpr}${renderChildScopeArgs(node, keyArg)})}`
2736
2848
  }
2737
2849
 
2738
2850
  case 'loop': {
@@ -2800,7 +2912,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2800
2912
  // its own props context via recurseInLoopBody.
2801
2913
  const body = renderPreamble(node.flatMapCallback, {
2802
2914
  textVariant: 'template',
2803
- transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null),
2915
+ transformJs: (t) => rewritePropsObjectRef(t, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: childScope }),
2804
2916
  // Leaf `key` stripped — see the irToHtmlTemplate site above.
2805
2917
  renderLeaf: (ir) => recurseInLoopBody(stripLeafKeyAttr(ir)),
2806
2918
  })
@@ -2808,7 +2920,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2808
2920
  } else if (node.preamble) {
2809
2921
  // Stage 3 / D4 — template-variant js text with the props rewrite
2810
2922
  // applied per segment; JSX leaves render via the loop-body recursion.
2811
- const preamble = renderPreamble(node.preamble, { textVariant: 'template', transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null), renderLeaf: (ir) => recurseInLoopBody(ir) })
2923
+ const preamble = renderPreamble(node.preamble, { textVariant: 'template', transformJs: (t) => rewritePropsObjectRef(t, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: childScope }), renderLeaf: (ir) => recurseInLoopBody(ir) })
2812
2924
  mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
2813
2925
  } else {
2814
2926
  mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => \`${childTemplate}\`).join('')}`
@@ -23,6 +23,10 @@ export const RUNTIME_IMPORT_CANDIDATES = [
23
23
  // falling through to the global store. The clone-root counterpart of the
24
24
  // mount point `createComponent` consumes for component-root rows.
25
25
  'mountRowRoot',
26
+ // Ambient parent-scope override for a static loop's materialize `forEach`
27
+ // (#2833) — that codepath runs during `init`, after the parent's own
28
+ // template evaluation (and its `_parentScopeId`) has already unwound.
29
+ 'withParentScope',
26
30
  'createPortal',
27
31
  'provideContext', 'createContext', 'useContext',
28
32
  'forwardProps', 'applyRestAttrs', 'splitProps', 'spreadAttrs', 'styleToCss', 'escapeAttr', 'escapeText', 'escapeTextOrNode',
@@ -18,7 +18,7 @@ import { canGenerateStaticTemplate, irToComponentTemplate, generateCsrTemplate }
18
18
  import { PROPS_PARAM } from './utils.ts'
19
19
  import { buildInlinableConstants, csrInlinableConstantsFromCtx } from './emit-registration.ts'
20
20
  import { buildEnvFromCtx } from './compute-inlinability.ts'
21
- import { nameForRegistryRef } from './component-scope.ts'
21
+ import { nameForRegistryRef, buildImportAliasMap, setActiveImportAliases } from './component-scope.ts'
22
22
  import { resolveRestSpreadNames } from './prop-handling.ts'
23
23
  import { IMPORT_PLACEHOLDER, RUNTIME_MODULE, detectUsedImports, collectExternalImports } from './imports.ts'
24
24
  import { isInlinableInTemplate } from '../relocate.ts'
@@ -85,32 +85,43 @@ export function generateClientJsWithSourceMap(
85
85
  adapterCapabilities?: AdapterCapabilities,
86
86
  profile?: boolean,
87
87
  ): ClientJsResult {
88
- const ctx = createContext(ir, scope, adapterCapabilities, profile)
89
- const siblingOffsets = computeLoopSiblingOffsets(ir.root)
90
- collectElements(ir.root, ctx, siblingOffsets)
88
+ // #2777 installed for the whole body (not just via `compiler.ts`'s
89
+ // `setActiveComponentScope`) because this function is also called
90
+ // directly, bypassing `compiler.ts` entirely: the CSR-conformance
91
+ // harness (`csr-render.ts`) and `analyzeClientNeeds` both do. Cleared
92
+ // in `finally` so a thrown error never leaks the alias map into an
93
+ // unrelated later compile.
94
+ setActiveImportAliases(buildImportAliasMap(ir.metadata.imports))
95
+ try {
96
+ const ctx = createContext(ir, scope, adapterCapabilities, profile)
97
+ const siblingOffsets = computeLoopSiblingOffsets(ir.root)
98
+ collectElements(ir.root, ctx, siblingOffsets)
91
99
 
92
- // Both `generateTemplateOnlyMount` and `generateInitFunction` run inline
93
- // analysis passes (buildInlinableConstants → computeInlinability) that
94
- // can add to `ctx.warnings` — most notably the BF060/BF061 stage-violation
95
- // diagnostics. Flush warnings to `ir.errors` AFTER those passes so the
96
- // diagnostics survive to the caller.
97
- if (!needsClientJs(ctx)) {
98
- const code = generateTemplateOnlyMount(ir, ctx)
100
+ // Both `generateTemplateOnlyMount` and `generateInitFunction` run inline
101
+ // analysis passes (buildInlinableConstants → computeInlinability) that
102
+ // can add to `ctx.warnings` — most notably the BF060/BF061 stage-violation
103
+ // diagnostics. Flush warnings to `ir.errors` AFTER those passes so the
104
+ // diagnostics survive to the caller.
105
+ if (!needsClientJs(ctx)) {
106
+ const code = generateTemplateOnlyMount(ir, ctx)
107
+ ir.errors.push(...ctx.warnings)
108
+ return { code }
109
+ }
110
+
111
+ const code = generateInitFunction(ir, ctx, siblingComponents, localImportPrefixes)
99
112
  ir.errors.push(...ctx.warnings)
100
- return { code }
101
- }
102
113
 
103
- const code = generateInitFunction(ir, ctx, siblingComponents, localImportPrefixes)
104
- ir.errors.push(...ctx.warnings)
114
+ if (options?.sourceMaps && code) {
115
+ const fileName = options.generatedFileName ?? `${ir.metadata.componentName}.client.js`
116
+ const sourceMap = buildSourceMapFromIR(code, ir, fileName)
117
+ const codeWithUrl = code + `\n//# sourceMappingURL=${fileName}.map`
118
+ return { code: codeWithUrl, sourceMap }
119
+ }
105
120
 
106
- if (options?.sourceMaps && code) {
107
- const fileName = options.generatedFileName ?? `${ir.metadata.componentName}.client.js`
108
- const sourceMap = buildSourceMapFromIR(code, ir, fileName)
109
- const codeWithUrl = code + `\n//# sourceMappingURL=${fileName}.map`
110
- return { code: codeWithUrl, sourceMap }
121
+ return { code }
122
+ } finally {
123
+ setActiveImportAliases(null)
111
124
  }
112
-
113
- return { code }
114
125
  }
115
126
 
116
127
  /**
@@ -275,7 +286,7 @@ function generateTemplateOnlyMount(ir: ComponentIR, ctx: ClientJsContext): strin
275
286
  // matches `emitRegistrationAndHydration`'s derivation byte-for-byte
276
287
  // rather than special-casing "no markup slots here" (#2651).
277
288
  const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
278
- templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds)
289
+ templateHtml = irToComponentTemplate(ir.root, inlinableConstants, restSpreadNames, ctx.propsObjectName, markupSlotIds, ctx.restPropsName)
279
290
  }
280
291
 
281
292
  // CSR fallback: when static template generation fails (e.g., components with