@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.
- package/dist/adapters/child-scope.d.ts +13 -0
- package/dist/adapters/child-scope.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +17 -0
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +17 -4
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1399 -1173
- package/dist/ir-to-client-js/child-components.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/component-scope.d.ts +20 -0
- package/dist/ir-to-client-js/component-scope.d.ts.map +1 -1
- package/dist/ir-to-client-js/compute-inlinability.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +4 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +35 -9
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +10 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +49 -2
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts +12 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/lowering-registry.d.ts +12 -0
- package/dist/lowering-registry.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +5 -2
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +49 -11
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +15 -15
- package/src/__tests__/aliased-import-child-registry-2777.test.ts +112 -0
- package/src/__tests__/date-lowering.test.ts +18 -4
- package/src/__tests__/inner-loop-mathml-namespace.test.ts +1 -1
- package/src/__tests__/inner-loop-svg-namespace.test.ts +4 -4
- package/src/__tests__/issue-2724-prop-alias-loop-not-static.test.ts +355 -0
- package/src/__tests__/issue-2737-props-object-alias-template.test.ts +86 -0
- package/src/__tests__/issue-2756-input-skeleton-bails.test.ts +123 -0
- package/src/__tests__/issue-2778-getter-alias-csr-template.test.ts +113 -0
- package/src/__tests__/issue-2806-rest-bag-template-rewrite.test.ts +136 -0
- package/src/__tests__/issue-2835-props-type-member-name-collision.test.ts +259 -0
- package/src/__tests__/loop-item-root-scope.test.ts +7 -3
- package/src/__tests__/lowering-value-position.test.ts +232 -0
- package/src/__tests__/markup-prop-brand.test.ts +165 -26
- package/src/__tests__/namespace-import-primitive.test.ts +244 -0
- package/src/__tests__/rewrite-destructured-props.test.ts +53 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +2 -2
- package/src/adapters/child-scope.ts +23 -0
- package/src/adapters/parsed-expr-emitter.ts +43 -0
- package/src/analyzer.ts +130 -0
- package/src/errors.ts +14 -1
- package/src/expression-parser.ts +63 -29
- package/src/index.ts +9 -1
- package/src/ir-to-client-js/child-components.ts +8 -1
- package/src/ir-to-client-js/collect-elements.ts +10 -38
- package/src/ir-to-client-js/component-scope.ts +60 -0
- package/src/ir-to-client-js/compute-inlinability.ts +46 -11
- package/src/ir-to-client-js/control-flow/shared.ts +12 -5
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
- package/src/ir-to-client-js/csr-substitute.ts +82 -16
- package/src/ir-to-client-js/emit-reactive.ts +29 -13
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +141 -70
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/ir-to-client-js/index.ts +34 -23
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +7 -2
- package/src/ir-to-client-js/rewrite-props-object.ts +14 -0
- package/src/jsx-to-ir.ts +200 -42
- package/src/lowering-registry.ts +21 -0
- package/src/prop-rewrite.ts +23 -5
- package/src/props-binding.ts +74 -18
- 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,
|
|
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 {
|
|
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
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
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`)
|
|
1083
|
-
//
|
|
1084
|
-
// (#
|
|
1085
|
-
//
|
|
1086
|
-
|
|
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
|
-
*
|
|
1641
|
-
* `
|
|
1642
|
-
* `
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
* `
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
* `
|
|
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
|
|
1655
|
-
|
|
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)
|
|
1670
|
-
//
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
|
|
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
|
|
2520
|
-
//
|
|
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
|
|
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
|
-
|
|
2776
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|