@barefootjs/jsx 0.33.4 → 0.34.0

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 (83) 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/adapters/parsed-expr-emitter.d.ts +17 -0
  4. package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/errors.d.ts +1 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/expression-parser.d.ts +17 -4
  9. package/dist/expression-parser.d.ts.map +1 -1
  10. package/dist/index.d.ts +4 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1399 -1173
  13. package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  15. package/dist/ir-to-client-js/component-scope.d.ts +20 -0
  16. package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/compute-inlinability.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/loop.d.ts.map +1 -1
  21. package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
  22. package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
  23. package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
  24. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  25. package/dist/ir-to-client-js/html-template.d.ts +49 -2
  26. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  27. package/dist/ir-to-client-js/imports.d.ts +2 -2
  28. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  29. package/dist/ir-to-client-js/index.d.ts.map +1 -1
  30. package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
  31. package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
  32. package/dist/jsx-to-ir.d.ts.map +1 -1
  33. package/dist/lowering-registry.d.ts +12 -0
  34. package/dist/lowering-registry.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 +6 -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__/inner-loop-mathml-namespace.test.ts +1 -1
  46. package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
  47. package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
  48. package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
  49. package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
  50. package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
  51. package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
  52. package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
  53. package/src/__tests__/loop-item-root-scope.test.ts +7 -3
  54. package/src/__tests__/lowering-value-position.test.ts +232 -0
  55. package/src/__tests__/markup-prop-brand.test.ts +165 -26
  56. package/src/__tests__/namespace-import-primitive.test.ts +244 -0
  57. package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
  58. package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
  59. package/src/adapters/child-scope.ts +23 -0
  60. package/src/adapters/parsed-expr-emitter.ts +43 -0
  61. package/src/analyzer.ts +130 -0
  62. package/src/errors.ts +14 -1
  63. package/src/expression-parser.ts +63 -29
  64. package/src/index.ts +9 -1
  65. package/src/ir-to-client-js/child-components.ts +8 -1
  66. package/src/ir-to-client-js/collect-elements.ts +10 -38
  67. package/src/ir-to-client-js/component-scope.ts +60 -0
  68. package/src/ir-to-client-js/compute-inlinability.ts +46 -11
  69. package/src/ir-to-client-js/control-flow/shared.ts +12 -5
  70. package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
  71. package/src/ir-to-client-js/csr-substitute.ts +82 -16
  72. package/src/ir-to-client-js/emit-reactive.ts +29 -13
  73. package/src/ir-to-client-js/emit-registration.ts +1 -1
  74. package/src/ir-to-client-js/html-template.ts +141 -70
  75. package/src/ir-to-client-js/imports.ts +4 -0
  76. package/src/ir-to-client-js/index.ts +34 -23
  77. package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
  78. package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
  79. package/src/jsx-to-ir.ts +200 -42
  80. package/src/lowering-registry.ts +21 -0
  81. package/src/prop-rewrite.ts +23 -5
  82. package/src/props-binding.ts +74 -18
  83. package/src/types.ts +6 -5
@@ -8,11 +8,12 @@ import { toHtmlAttrName, attrValueToString, quotePropName, PROPS_PARAM, DATA_BF_
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
  /**
@@ -658,10 +659,11 @@ function buildSpreadAttrsMergeCall(args: {
658
659
  * are wrapped with `__bfSlot(EXPR, <branchSlotsVar>)` so the runtime can
659
660
  * splice live `Node` returns into the parsed fragment instead of
660
661
  * stringifying them via the template literal (#1213).
661
- * Component nodes omit their `slotId` from the `renderChild` call exactly
662
- * when `derivesScopeFromSlot()` says so — a loop-item-root component (its
663
- * own scope, identified by `data-key`) never derives from the parent slot;
664
- * 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
665
667
  * `IRComponent.loopItemRoot` / `./adapters/child-scope.ts`.
666
668
  */
667
669
  /**
@@ -1079,12 +1081,11 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: ReadonlySet<str
1079
1081
  const keyArg = keyProp ? `, ${attrValueToString(keyProp.value) ?? 'undefined'}` : ''
1080
1082
  // Pass slotId as suffix so $c() can find the child component by slot
1081
1083
  // after branch swap. A loop-item-root component owns a separate scope
1082
- // per iteration (identified by `data-key`), so the parent-slot suffix
1083
- // is dropped to avoid anchoring it to a wrong component-wrapper scope
1084
- // (#1268) a component nested BELOW the row root still derives from
1085
- // its slot, matching the Hono reference (#2444).
1086
- const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
1087
- 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)})}`
1088
1089
  }
1089
1090
 
1090
1091
  case 'loop': {
@@ -1230,6 +1231,21 @@ export function buildLoopSkeletonTemplate(node: IRNode, safe: LoopSkeletonSafeSl
1230
1231
  case 'template': {
1231
1232
  const attrKey = node.slotId ? `${node.slotId}::${a.name}` : null
1232
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
1233
1249
  // Covered by a loop-child createEffect — omit from the skeleton
1234
1250
  // entirely; the effect's eager first run fills it in.
1235
1251
  break
@@ -1628,34 +1644,52 @@ export function irToPlaceholderTemplate(node: IRNode, restSpreadNames?: Readonly
1628
1644
  * - IRConditional → ternary expression
1629
1645
  * - Single child → single expression; multiple → array literal
1630
1646
  */
1631
- export function irChildrenToJsExpr(children: IRNode[]): string {
1647
+ export function irChildrenToJsExpr(children: IRNode[], brandMarkup = false): string {
1632
1648
  // Flatten fragments and filter empty text nodes
1633
- const exprs = children.flatMap(c => irNodeToJsExprs(c)).filter(Boolean)
1649
+ const exprs = children.flatMap(c => irNodeToJsExprs(c, brandMarkup)).filter(Boolean)
1634
1650
  if (exprs.length === 0) return "''"
1635
1651
  if (exprs.length === 1) return exprs[0]
1636
1652
  return `[${exprs.join(', ')}]`
1637
1653
  }
1638
1654
 
1639
1655
  /**
1640
- * Narrow gate for branding a nested `jsx-children` getter's value with
1641
- * `bfMarkup()` (#2651) — the `irNodeToJsExprs` counterpart of
1642
- * `collect-elements.ts`'s `isSingleElementJsxChildren` (duplicated rather
1643
- * than imported: `collect-elements.ts` already imports FROM this file, so
1644
- * the reverse import would cycle). Same shape, same reasoning: a
1645
- * `jsx-children` prop's value is always a single stored node
1646
- * (`AttrValueOf.jsxChildren([...])`); only when that node is a lone
1647
- * `'element'` does `irChildrenToJsExpr` reduce it to ONE HTML-string
1648
- * template literal the shape `bfMarkup` + `escapeTextOrNode` /
1649
- * `escapeTextOrMarkup` have a proven contract for. A `'fragment'` (multiple
1650
- * children) or `'conditional'` node reduces to an array literal or a
1651
- * nested ternary instead — left unbranded here for the same reason as the
1652
- * `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.
1653
1685
  */
1654
- function isSingleElementJsxChildren(nodes: IRNode[]): boolean {
1655
- 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)
1656
1690
  }
1657
1691
 
1658
- function irNodeToJsExprs(node: IRNode): string[] {
1692
+ function irNodeToJsExprs(node: IRNode, brandMarkup = false): string[] {
1659
1693
  switch (node.type) {
1660
1694
  case 'component': {
1661
1695
  const propsEntries: string[] = node.props
@@ -1666,17 +1700,10 @@ function irNodeToJsExprs(node: IRNode): string[] {
1666
1700
  }
1667
1701
  switch (p.value.kind) {
1668
1702
  case 'jsx-children': {
1669
- // Brand (#2651) only the lone-'element' shape, and never an
1670
- // explicit `children={<jsx/>}` prop — see
1671
- // `isSingleElementJsxChildren` (this file) for the shape
1672
- // argument and the `collect-elements.ts` twin for the
1673
- // `children`-exclusion argument (bare-passthrough consumer,
1674
- // no unwrap call).
1675
- const jsxExpr = irChildrenToJsExpr(p.value.children)
1676
- const wrapped = (p.name !== 'children' && isSingleElementJsxChildren(p.value.children))
1677
- ? `bfMarkup(${jsxExpr})`
1678
- : jsxExpr
1679
- 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} }`
1680
1707
  }
1681
1708
  case 'literal':
1682
1709
  return `get ${quotePropName(p.name)}() { return ${JSON.stringify(p.value.value)} }`
@@ -1707,14 +1734,16 @@ function irNodeToJsExprs(node: IRNode): string[] {
1707
1734
  if (!node.value.trim()) return []
1708
1735
  return [JSON.stringify(node.value)]
1709
1736
 
1710
- case 'element':
1711
- return [`\`${irToHtmlTemplate(node)}\``]
1737
+ case 'element': {
1738
+ const html = `\`${irToHtmlTemplate(node)}\``
1739
+ return [brandMarkup ? `bfMarkup(${html})` : html]
1740
+ }
1712
1741
 
1713
1742
  case 'fragment':
1714
- return node.children.flatMap(c => irNodeToJsExprs(c))
1743
+ return node.children.flatMap(c => irNodeToJsExprs(c, brandMarkup))
1715
1744
 
1716
1745
  case 'conditional':
1717
- return [`${node.condition} ? ${irChildrenToJsExpr([node.whenTrue])} : ${irChildrenToJsExpr([node.whenFalse])}`]
1746
+ return [`${node.condition} ? ${irChildrenToJsExpr([node.whenTrue], brandMarkup)} : ${irChildrenToJsExpr([node.whenFalse], brandMarkup)}`]
1718
1747
 
1719
1748
  // The kinds below previously fell through to `default: return []` and so
1720
1749
  // were silently dropped when used as a component child. The explicit
@@ -1771,6 +1800,21 @@ export interface TemplateOptions {
1771
1800
  inlinableConstants?: Map<string, string>
1772
1801
  restSpreadNames?: ReadonlySet<string>
1773
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
1774
1818
  /**
1775
1819
  * Names that exist only in the init-body scope (or were demoted to unsafe
1776
1820
  * during chained-ref resolution). The CSR template runs at module scope
@@ -1862,13 +1906,14 @@ export function irToComponentTemplate(
1862
1906
  inlinableConstants?: Map<string, string>,
1863
1907
  restSpreadNames?: ReadonlySet<string>,
1864
1908
  propsObjectName?: string | null,
1865
- markupSlotIds?: ReadonlySet<string>
1909
+ markupSlotIds?: ReadonlySet<string>,
1910
+ restPropsName?: string | null
1866
1911
  ): string {
1867
- return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1, markupSlotIds })
1912
+ return irToComponentTemplateWithOpts(node, { inlinableConstants, restSpreadNames, propsObjectName, loopDepth: -1, markupSlotIds, restPropsName })
1868
1913
  }
1869
1914
 
1870
1915
  function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
1871
- const { inlinableConstants, restSpreadNames, propsObjectName, loopDepth = 0 } = opts
1916
+ const { inlinableConstants, restSpreadNames, propsObjectName, restPropsName, loopDepth = 0 } = opts
1872
1917
  // `recurse` preserves `inHoistedChildren` for structural pass-through
1873
1918
  // (fragment / conditional); `childrenRecurse` clears it so element
1874
1919
  // descendants don't re-emit the placeholder. (#1320)
@@ -1889,15 +1934,31 @@ function irToComponentTemplateWithOpts(node: IRNode, opts: TemplateOptions): str
1889
1934
  }
1890
1935
  }
1891
1936
 
1892
- // Normalize source-level props object access (e.g., props.xxx → _p.xxx)
1893
- if (propsObjectName && propsObjectName !== PROPS_PARAM) {
1894
- result = result.replace(
1895
- new RegExp(`\\b${propsObjectName}\\.`, 'g'),
1896
- `${PROPS_PARAM}.`,
1897
- )
1898
- }
1899
-
1900
- 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
1901
1962
  }
1902
1963
 
1903
1964
  switch (node.type) {
@@ -2274,8 +2335,11 @@ export function generateCsrTemplate(
2274
2335
  // Build the substitution env once per component. Signals + memos come
2275
2336
  // from `buildSignalMemoEnv`; inlinable constants layer in here so
2276
2337
  // each call to `transformExpr` is a pure AST projection over the
2277
- // shared env (no per-call Map cloning).
2278
- 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)
2279
2343
  const csrEnv: CsrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName }
2280
2344
  if (inlinableConstants) {
2281
2345
  for (const [name, value] of inlinableConstants) {
@@ -2292,7 +2356,7 @@ export function generateCsrTemplate(
2292
2356
  // by construction (`collectElements`'s `expression` visitor only pushes
2293
2357
  // here). Reused as-is, not re-derived.
2294
2358
  const markupSlotIds = new Set(ctx.dynamicElements.map(e => e.slotId))
2295
- 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 })
2296
2360
  }
2297
2361
 
2298
2362
  /**
@@ -2369,7 +2433,7 @@ function buildCsrEnvForCtx(
2369
2433
  inlinableConstants: Map<string, string> | undefined,
2370
2434
  propsObjectName?: string | null,
2371
2435
  ): CsrEnv {
2372
- const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null)
2436
+ const base = buildSignalMemoEnv(ctx.signals, ctx.memos, propsObjectName ?? null, ctx.localConstants)
2373
2437
  const csrEnv: CsrEnv = { substitutions: new Map(base.substitutions), propsObjectName: base.propsObjectName }
2374
2438
  if (inlinableConstants) {
2375
2439
  for (const [name, value] of inlinableConstants) {
@@ -2487,7 +2551,7 @@ export function computeDeferredChildSlots(
2487
2551
  }
2488
2552
 
2489
2553
  function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): string {
2490
- const { restSpreadNames, propsObjectName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
2554
+ const { restSpreadNames, propsObjectName, restPropsName, csrEnv, unsafeLocalNames, loopDepth = 0 } = opts
2491
2555
  const env: CsrEnv = csrEnv ?? { substitutions: new Map(), propsObjectName: propsObjectName ?? null }
2492
2556
  const transformExpr = (expr: string, templateExpr?: string): string => {
2493
2557
  // Single AST substitution pass: replaces signal getter calls
@@ -2516,11 +2580,16 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2516
2580
  if (unsafeLocalNames && unsafeLocalNames.size > 0 && setIntersects(freeIdentifiers, unsafeLocalNames)) {
2517
2581
  return UNSAFE_TEMPLATE_EXPR
2518
2582
  }
2519
- // Final emit-form: rewrite `propsName.X → _p.X`. Deferred until
2520
- // 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
2521
2590
  // (consistent with `populateCsrInlinable`'s `isInlinableInTemplate`
2522
2591
  // gate — #1138).
2523
- return applyPropsRewrite(rewritten, propsObjectName ?? null)
2592
+ return rewritePropsObjectRef(rewritten, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: opts.scope })
2524
2593
  }
2525
2594
 
2526
2595
  // `recurse` preserves `inHoistedChildren` for structural pass-through
@@ -2772,8 +2841,10 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2772
2841
  const propsExpr = propsEntries.length > 0 ? `{${propsEntries.join(', ')}}` : '{}'
2773
2842
  const keyProp = node.props.find(p => p.name === 'key')
2774
2843
  const keyArg = keyProp ? `, ${transformKeyValue(keyProp.value, transformExpr)}` : ''
2775
- const slotArg = derivesScopeFromSlot(node) ? `, '${node.slotId}'` : ''
2776
- 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)})}`
2777
2848
  }
2778
2849
 
2779
2850
  case 'loop': {
@@ -2841,7 +2912,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2841
2912
  // its own props context via recurseInLoopBody.
2842
2913
  const body = renderPreamble(node.flatMapCallback, {
2843
2914
  textVariant: 'template',
2844
- transformJs: (t) => applyPropsRewrite(t, propsObjectName ?? null),
2915
+ transformJs: (t) => rewritePropsObjectRef(t, propsObjectName ?? null, restPropsName ?? null, { enclosingScope: childScope }),
2845
2916
  // Leaf `key` stripped — see the irToHtmlTemplate site above.
2846
2917
  renderLeaf: (ir) => recurseInLoopBody(stripLeafKeyAttr(ir)),
2847
2918
  })
@@ -2849,7 +2920,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
2849
2920
  } else if (node.preamble) {
2850
2921
  // Stage 3 / D4 — template-variant js text with the props rewrite
2851
2922
  // applied per segment; JSX leaves render via the loop-body recursion.
2852
- 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) })
2853
2924
  mapExpr = `\${${iterArrayExpr}.${iterMethod}(${callbackParam} => { ${preamble} return \`${childTemplate}\` }).join('')}`
2854
2925
  } else {
2855
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
@@ -20,7 +20,7 @@
20
20
  import type { IRLoopChildComponent, MapCallbackPreamble } from '../../types.ts'
21
21
  import type { ClientJsContext, NestedLoop, TopLevelLoop } from '../types.ts'
22
22
  import { quotePropName, varSlotId, attrValueToString, buildLoopChildIndexExpr } from '../utils.ts'
23
- import { irChildrenToJsExpr, renderPreamble, irToHtmlTemplate } from '../html-template.ts'
23
+ import { jsxChildrenPropGetterExpr, renderPreamble, irToHtmlTemplate } from '../html-template.ts'
24
24
 
25
25
  /**
26
26
  * Render a loop preamble for the static-array init context. Static `forEach`
@@ -232,7 +232,12 @@ function buildStaticPropsExpr(props: readonly LoopChildCompProp[]): PropsExpr {
232
232
  case 'boolean-attr':
233
233
  return `${quotePropName(p.name)}: true`
234
234
  case 'jsx-children':
235
- return `get ${quotePropName(p.name)}() { return ${irChildrenToJsExpr(p.value.children)} }`
235
+ // Brand via the shared door (#2651/#2702) — see
236
+ // `jsxChildrenPropGetterExpr`'s docstring. A named jsx-children
237
+ // prop on a component instantiated inside a static-array `.map()`
238
+ // loop used to reach its getter unbranded (pullfrog review, PR
239
+ // #2820).
240
+ return `get ${quotePropName(p.name)}() { return ${jsxChildrenPropGetterExpr(p.value.children, p.name)} }`
236
241
  case 'expression':
237
242
  case 'template':
238
243
  case 'spread':
@@ -26,6 +26,18 @@
26
26
  import ts from 'typescript'
27
27
  import { PROPS_PARAM } from './utils.ts'
28
28
  import { identifierPattern } from '../identifier-pattern.ts'
29
+ import type { BindingScope } from '../scope/binding-scope.ts'
30
+
31
+ export interface PropsObjectRewriteOptions {
32
+ /**
33
+ * SHADOW GUARD (CLAUDE.md's `BindingScope` distinction — `isBound()`,
34
+ * not `valueBoundNames()`): a `.map(props => ...)` row whose callback
35
+ * PARAMETER happens to be named the same as the outer `propsObjectName`
36
+ * must not have that parameter rewritten to `_p` — it shadows the outer
37
+ * props object inside its own row, same as any other bound name would.
38
+ */
39
+ enclosingScope?: BindingScope
40
+ }
29
41
 
30
42
  /**
31
43
  * Rename every value-position reference to `propsObjectName` — and,
@@ -64,11 +76,13 @@ export function rewritePropsObjectRef(
64
76
  code: string,
65
77
  propsObjectName: string | null,
66
78
  restPropsName: string | null = null,
79
+ opts?: PropsObjectRewriteOptions,
67
80
  ): string {
68
81
  let result = code
69
82
  const seen = new Set<string>()
70
83
  for (const srcPropsName of [propsObjectName ?? 'props', restPropsName]) {
71
84
  if (srcPropsName === null || srcPropsName === PROPS_PARAM || seen.has(srcPropsName)) continue
85
+ if (opts?.enclosingScope?.isBound(srcPropsName)) continue
72
86
  seen.add(srcPropsName)
73
87
  result = rewriteOneName(result, srcPropsName)
74
88
  }