@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
package/src/jsx-to-ir.ts
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from './types.ts'
|
|
41
41
|
import { type AnalyzerContext, type MultiReturnJsxInfo, getSourceLocation, collectReactiveGetterNames } from './analyzer-context.ts'
|
|
42
42
|
import { parseExpression, isSupported, parseBlockBody, foldBlockToExpr, predicateTernaryToLogical, tsNodeToParsedExpr, sortComparatorFromArrow, stringifyParsedExpr, cssKebabCase, CALLBACK_METHODS, type ParsedExpr } from './expression-parser.ts'
|
|
43
|
-
import type { IRLoopSort, FunctionInfo } from './types.ts'
|
|
43
|
+
import type { IRLoopSort, FunctionInfo, ConstantInfo } from './types.ts'
|
|
44
44
|
import { formatParamWithType } from './module-exports.ts'
|
|
45
45
|
import { createError, ErrorCodes, internalInvariant } from './errors.ts'
|
|
46
46
|
import { CLIENT_BUILTIN_SOURCE, isClientBuiltinName, type ClientBuiltinTag } from './builtins.ts'
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
rewriteBarePropRefs as rewriteBarePropRefsCore,
|
|
50
50
|
collectAstPropRefs,
|
|
51
51
|
} from './prop-rewrite.ts'
|
|
52
|
-
import { buildPropAliasMap, resolveRestSpreadOriginCore } from './props-binding.ts'
|
|
52
|
+
import { boundPropLocalNames, buildPropAliasMap, resolveAliasOrigin, resolveRestSpreadOriginCore } from './props-binding.ts'
|
|
53
53
|
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
54
54
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
55
55
|
import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template.ts'
|
|
@@ -73,6 +73,7 @@ import { identifierPattern, identifierCallPattern } from './identifier-pattern.t
|
|
|
73
73
|
interface ReactivityPatterns {
|
|
74
74
|
signals: { getter: string; pattern: RegExp }[]
|
|
75
75
|
memos: { name: string; pattern: RegExp }[]
|
|
76
|
+
/** For regex `props.<key>` detection only — for exact-name binding checks read `TransformContext.boundPropNames` instead (#2835). */
|
|
76
77
|
props: { name: string; pattern: RegExp }[]
|
|
77
78
|
constants: { name: string; value: string | undefined; pattern: RegExp }[]
|
|
78
79
|
}
|
|
@@ -85,6 +86,14 @@ interface TransformContext {
|
|
|
85
86
|
isRoot: boolean
|
|
86
87
|
insideComponentChildren: boolean
|
|
87
88
|
patterns: ReactivityPatterns
|
|
89
|
+
/**
|
|
90
|
+
* Local names actually bound by the props parameter (#2835) — unlike
|
|
91
|
+
* `patterns.props`, which for a whole `(props: Props)` parameter holds
|
|
92
|
+
* every TYPE-MEMBER name (needed so its regex `pattern` still matches
|
|
93
|
+
* `props.<key>` accesses), this is empty in that shape since none of
|
|
94
|
+
* those names are local bindings a shadow check could ever see.
|
|
95
|
+
*/
|
|
96
|
+
boundPropNames: ReadonlySet<string>
|
|
88
97
|
/** Shortcut for analyzer.getJS(node) */
|
|
89
98
|
getJS(node: ts.Node): string
|
|
90
99
|
/** getJS + rewrite destructured prop refs for client JS templates (#807) */
|
|
@@ -93,8 +102,20 @@ interface TransformContext {
|
|
|
93
102
|
_reactiveGetterNames?: Set<string>
|
|
94
103
|
/** Cached set of module-scope @client signal/memo names. */
|
|
95
104
|
_moduleClientSignalNames?: Set<string>
|
|
96
|
-
/**
|
|
97
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Cached `localConstants` index (name → value text), shared by every
|
|
107
|
+
* `resolveAliasOrigin` walk in this file — `forwardsCallerRestProps`'s
|
|
108
|
+
* rest/props-spread alias walk and `isArrayExprDirectPropRef`'s
|
|
109
|
+
* direct-prop-array alias walk (#2724).
|
|
110
|
+
*/
|
|
111
|
+
_constantValuesByName?: ReadonlyMap<string, string | undefined>
|
|
112
|
+
/**
|
|
113
|
+
* Cached `localConstants` index (name → full `ConstantInfo`), for
|
|
114
|
+
* lookups that need more than the value text — e.g.
|
|
115
|
+
* `isArrayExprDirectPropRef` reading a constant's structured `.parsed`
|
|
116
|
+
* to recognize `const arr = props.items` (#2724).
|
|
117
|
+
*/
|
|
118
|
+
_constantsByName?: ReadonlyMap<string, ConstantInfo>
|
|
98
119
|
/** Cached set of destructured prop names for AST-based rewriting */
|
|
99
120
|
_destructuredPropNames?: Set<string> | null
|
|
100
121
|
/**
|
|
@@ -668,6 +689,10 @@ function getDestructuredPropAliases(ctx: TransformContext): Map<string, string>
|
|
|
668
689
|
}
|
|
669
690
|
|
|
670
691
|
function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
692
|
+
// Shared by `patterns.props` (regex matching) and `boundPropNames`
|
|
693
|
+
// (exact-name binding checks, #2835) so both agree on which prop
|
|
694
|
+
// params are in play — `children` excluded from both the same way.
|
|
695
|
+
const propsParamsForMatching = analyzer.propsParams.filter(p => p.name !== 'children')
|
|
671
696
|
return {
|
|
672
697
|
analyzer,
|
|
673
698
|
sourceFile: analyzer.sourceFile,
|
|
@@ -681,6 +706,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
681
706
|
insideComponentChildren: false,
|
|
682
707
|
scope: BindingScope.EMPTY,
|
|
683
708
|
loopDepth: 0,
|
|
709
|
+
boundPropNames: boundPropLocalNames({ propsParams: propsParamsForMatching, propsObjectName: analyzer.propsObjectName }),
|
|
684
710
|
patterns: {
|
|
685
711
|
signals: analyzer.signals.map(s => ({
|
|
686
712
|
getter: s.getter,
|
|
@@ -690,9 +716,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
690
716
|
name: m.name,
|
|
691
717
|
pattern: identifierCallPattern(m.name),
|
|
692
718
|
})),
|
|
693
|
-
props:
|
|
694
|
-
.filter(p => p.name !== 'children')
|
|
695
|
-
.map(p => ({ name: p.name, pattern: identifierPattern(p.name) })),
|
|
719
|
+
props: propsParamsForMatching.map(p => ({ name: p.name, pattern: identifierPattern(p.name) })),
|
|
696
720
|
constants: analyzer.localConstants.map(c => ({
|
|
697
721
|
name: c.name,
|
|
698
722
|
value: c.value,
|
|
@@ -5479,8 +5503,8 @@ function buildFlatMapCallback(
|
|
|
5479
5503
|
// Template variant per segment, like buildPreambleSegments: the hydrate
|
|
5480
5504
|
// registration template is module scope (`template: (_p) => ...`), so a
|
|
5481
5505
|
// bare destructured-prop reference must rewrite to `_p.xxx` there —
|
|
5482
|
-
// `
|
|
5483
|
-
// object name).
|
|
5506
|
+
// `rewritePropsObjectRef` can't do it (destructured components have no
|
|
5507
|
+
// props object name to rewrite from).
|
|
5484
5508
|
const tpl = rewriteBarePropRefs(piece.js, body, ctx)
|
|
5485
5509
|
return tpl !== undefined && tpl !== piece.js
|
|
5486
5510
|
? { kind: 'js', text: piece.js, templateText: tpl }
|
|
@@ -7393,10 +7417,7 @@ function expressionWrapsJsx(node: ts.Expression): boolean {
|
|
|
7393
7417
|
* Refuse a component prop whose initializer is a ternary/array literally
|
|
7394
7418
|
* wrapping JSX (#2667) — see `expressionWrapsJsx`'s docstring and the call
|
|
7395
7419
|
* site above for why this can't fall through to the plain `expression`
|
|
7396
|
-
* AttrValue path (raw JSX syntax would splice into the emitted client
|
|
7397
|
-
* JS) and why the fragment-wrap escape is NOT offered here (BF021 message
|
|
7398
|
-
* below explains the unsoundness inline; see #2667's tracking issue for
|
|
7399
|
-
* the full door-inventory finding).
|
|
7420
|
+
* AttrValue path (raw JSX syntax would splice into the emitted client JS).
|
|
7400
7421
|
*/
|
|
7401
7422
|
function reportNakedJsxWrapperProp(
|
|
7402
7423
|
ctx: TransformContext,
|
|
@@ -7411,15 +7432,21 @@ function reportNakedJsxWrapperProp(
|
|
|
7411
7432
|
`Prop '${propName}' is ${shape} wrapping JSX (${jsxExpr.getText(ctx.sourceFile)}). ` +
|
|
7412
7433
|
`This shape is not compiled — only a JSX element/fragment given DIRECTLY as the prop value is.`,
|
|
7413
7434
|
suggestion: {
|
|
7414
|
-
message:
|
|
7415
|
-
`Move the conditional
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
`
|
|
7435
|
+
message: ts.isConditionalExpression(jsxExpr)
|
|
7436
|
+
? `Move the conditional out of the prop position: compute it in a local const and pass it as ` +
|
|
7437
|
+
`the component's children instead of a named prop ` +
|
|
7438
|
+
`(e.g. const ${propName} = ${jsxExpr.getText(ctx.sourceFile)}; <Comp>{${propName}}</Comp>). ` +
|
|
7439
|
+
`Wrapping the ternary in a fragment at the prop position ` +
|
|
7440
|
+
`(${propName}={<>{${jsxExpr.getText(ctx.sourceFile)}}</>}) is also a safe escape (#2702): the ` +
|
|
7441
|
+
`compiler brands each JSX branch individually, so the child's reactive prop getter unwraps it ` +
|
|
7442
|
+
`correctly instead of re-escaping it as text.`
|
|
7443
|
+
: `Move the array out of the prop position: compute it in a local const and pass it as the ` +
|
|
7444
|
+
`component's children instead of a named prop ` +
|
|
7445
|
+
`(e.g. const ${propName} = ${jsxExpr.getText(ctx.sourceFile)}; <Comp>{${propName}}</Comp>). ` +
|
|
7446
|
+
`Wrapping the array in a fragment at the prop position is NOT a safe escape here: it compiles, ` +
|
|
7447
|
+
`but flattens to more than one part, which the child's reactive prop getter has no branding ` +
|
|
7448
|
+
`contract for (a multi-part jsx-children shape, tracked separately from #2702's single-` +
|
|
7449
|
+
`conditional fix).`,
|
|
7423
7450
|
escape: [{ kind: 'rewrite' }],
|
|
7424
7451
|
},
|
|
7425
7452
|
}),
|
|
@@ -7474,14 +7501,13 @@ function processComponentProps(
|
|
|
7474
7501
|
// plain `expression` path below, which stringifies the initializer's
|
|
7475
7502
|
// SOURCE TEXT — literal JSX syntax spliced into the emitted client
|
|
7476
7503
|
// JS (invalid at runtime; the #2651 door inventory's discovery).
|
|
7477
|
-
// Refuse loudly instead of guessing a lowering
|
|
7478
|
-
// escape
|
|
7479
|
-
//
|
|
7480
|
-
//
|
|
7481
|
-
//
|
|
7482
|
-
//
|
|
7483
|
-
//
|
|
7484
|
-
// offered.
|
|
7504
|
+
// Refuse loudly instead of guessing a lowering. The fragment-wrap
|
|
7505
|
+
// escape IS safe for the ternary shape (#2702: `jsxChildrenPropGetterExpr`,
|
|
7506
|
+
// `ir-to-client-js/html-template.ts`, brands each JSX branch after the
|
|
7507
|
+
// fragment hoists the conditional) — `reportNakedJsxWrapperProp`
|
|
7508
|
+
// offers it for that case. It stays unsafe for the array-literal
|
|
7509
|
+
// shape (a multi-part jsx-children value has no branding contract),
|
|
7510
|
+
// so only the children-passthrough escape is offered there.
|
|
7485
7511
|
if (
|
|
7486
7512
|
(ts.isConditionalExpression(jsxExpr) || ts.isArrayLiteralExpression(jsxExpr)) &&
|
|
7487
7513
|
expressionWrapsJsx(jsxExpr)
|
|
@@ -7844,31 +7870,152 @@ function checkBareSignalOrMemoIdentifier(
|
|
|
7844
7870
|
* Structural AST check: is the array expression a direct prop reference?
|
|
7845
7871
|
*
|
|
7846
7872
|
* Returns true only when arrayExpr is:
|
|
7847
|
-
* (a) An Identifier that is a destructured prop binding (e.g. `toggleItems`)
|
|
7848
|
-
*
|
|
7873
|
+
* (a) An Identifier that is a destructured prop binding (e.g. `toggleItems`),
|
|
7874
|
+
* OR reaches one through a bare `x = y` local-alias-hop chain (e.g.
|
|
7875
|
+
* `const toggleItems__alias = toggleItems`, any number of hops, `const`
|
|
7876
|
+
* or `let`, #2724 — see `propAliasHopCandidates`'s docstring for why
|
|
7877
|
+
* `let` stays eligible)
|
|
7878
|
+
* (b) A PropertyAccessExpression rooted at the props object (e.g. `props.items`),
|
|
7879
|
+
* OR one whose object reaches the props object through the same kind of
|
|
7880
|
+
* alias-hop chain as (a) (e.g. `const p = props; p.items`, #2724 review)
|
|
7849
7881
|
*
|
|
7850
7882
|
* Unlike the regex-based `isPropsReference`, this avoids false positives from
|
|
7851
7883
|
* unrelated identifiers that happen to share a prop name (e.g. `state.items`
|
|
7852
|
-
* when `items` is also a prop)
|
|
7884
|
+
* when `items` is also a prop) — the alias-hop walk only ever follows a
|
|
7885
|
+
* constant's own bare-identifier value text or its structured `.parsed`
|
|
7886
|
+
* shape, never a substring/regex match against arbitrary expression text.
|
|
7887
|
+
* `propAliasHopCandidates` additionally restricts which constants are
|
|
7888
|
+
* eligible hop sources (see its docstring). A name that merely COLLIDES
|
|
7889
|
+
* with a prop's name without being bound to it (a module constant sharing a
|
|
7890
|
+
* whole-`props`-object type's member name) is excluded by the terminal
|
|
7891
|
+
* (`isDirectPropBindingName` reads `ctx.boundPropNames`, not the wider
|
|
7892
|
+
* `ctx.patterns.props` — #2835).
|
|
7893
|
+
*
|
|
7894
|
+
* This is more than a "prop reference somewhere in the expression" check
|
|
7895
|
+
* (`isPropsReference`'s regex semantics): the return value also feeds
|
|
7896
|
+
* `IRLoop.isPropDerivedArray`, which the Go adapter uses to decide whether a
|
|
7897
|
+
* nested component's field is literally the loop's prop-sourced data — a
|
|
7898
|
+
* false positive there would misclassify DSL-adapter codegen, not just add
|
|
7899
|
+
* one extra `mapArray`. So the alias walk stays structural (AST-derived
|
|
7900
|
+
* `ConstantInfo.parsed`), not a second regex-based implementation of
|
|
7901
|
+
* `isPropsReference`/`isSignalOrMemoReference`'s chain walk.
|
|
7853
7902
|
*/
|
|
7854
7903
|
function isArrayExprDirectPropRef(arrayExpr: ts.Expression, ctx: TransformContext): boolean {
|
|
7855
|
-
const propNames = new Set(ctx.patterns.props.map(p => p.name))
|
|
7856
7904
|
const propsObjName = ctx.analyzer.propsObjectName
|
|
7857
7905
|
|
|
7858
7906
|
if (ts.isIdentifier(arrayExpr)) {
|
|
7859
|
-
return
|
|
7907
|
+
return resolveAliasOrigin(propAliasHopCandidates(ctx), arrayExpr.text, name => (isDirectPropBindingName(name, ctx) ? true : null)) === true
|
|
7860
7908
|
}
|
|
7861
7909
|
|
|
7862
7910
|
if (ts.isPropertyAccessExpression(arrayExpr) && propsObjName) {
|
|
7863
7911
|
const obj = arrayExpr.expression
|
|
7864
|
-
if (ts.isIdentifier(obj)
|
|
7865
|
-
return true
|
|
7912
|
+
if (ts.isIdentifier(obj)) {
|
|
7913
|
+
return resolveAliasOrigin(propAliasHopCandidates(ctx), obj.text, name => (name === propsObjName ? true : null)) === true
|
|
7866
7914
|
}
|
|
7867
7915
|
}
|
|
7868
7916
|
|
|
7869
7917
|
return false
|
|
7870
7918
|
}
|
|
7871
7919
|
|
|
7920
|
+
/**
|
|
7921
|
+
* Does the local name `name` itself directly denote a prop — either a
|
|
7922
|
+
* destructured prop binding, or a local constant whose value is a bare
|
|
7923
|
+
* `<propsObjName>.<key>` member access (`const arr = props.items`)? The
|
|
7924
|
+
* alias-hop walk in `isArrayExprDirectPropRef` calls this at every hop; it
|
|
7925
|
+
* never inspects arbitrary expression text itself, only a single constant's
|
|
7926
|
+
* name (against the destructured-prop set) or its own structured `.parsed`
|
|
7927
|
+
* (against the props-object shape) — same "structural, not regex" guarantee
|
|
7928
|
+
* `isArrayExprDirectPropRef`'s docstring makes.
|
|
7929
|
+
*
|
|
7930
|
+
* Shadow-guarded FIRST (pullfrog review finding): `propAliasHopCandidates`
|
|
7931
|
+
* only keeps a shadowed name out of the map `resolveAliasOrigin` hops
|
|
7932
|
+
* INTO — it does nothing for `name` itself on the very first call, which
|
|
7933
|
+
* reads the separate, shadow-unaware `constantsByName(ctx)` index directly.
|
|
7934
|
+
* Without this guard, `rows.map((list, ri) => list.map(...))`'s inner
|
|
7935
|
+
* `list` — an arbitrary row value with no relation to props — would
|
|
7936
|
+
* resolve via an unrelated, shadowed outer `const list = props.entries`
|
|
7937
|
+
* the moment that outer constant's OWN value happens to be a
|
|
7938
|
+
* member-access-on-props shape, defeating the shadow guard entirely
|
|
7939
|
+
* (`propAliasHopCandidates`'s own `rows.map((list) => list.map(...))`
|
|
7940
|
+
* docstring example, just with a member-access-valued outer const instead
|
|
7941
|
+
* of an identifier-valued one — confirmed to reach real Go `isPropDerived`
|
|
7942
|
+
* codegen the same way, since Go's BF101 gate explicitly skips a
|
|
7943
|
+
* scope-bound array name).
|
|
7944
|
+
*
|
|
7945
|
+
* Reads `ctx.boundPropNames`, not `ctx.patterns.props` (#2835): for a whole
|
|
7946
|
+
* `(props: Props)` parameter, `patterns.props` holds every TYPE-MEMBER name
|
|
7947
|
+
* (needed so its regex still matches `props.<key>`) rather than local
|
|
7948
|
+
* bindings, so a same-named module const was matching here as if it were
|
|
7949
|
+
* the prop itself.
|
|
7950
|
+
*/
|
|
7951
|
+
function isDirectPropBindingName(name: string, ctx: TransformContext): boolean {
|
|
7952
|
+
if (ctx.scope.isBound(name)) return false
|
|
7953
|
+
if (ctx.boundPropNames.has(name)) return true
|
|
7954
|
+
const propsObjName = ctx.analyzer.propsObjectName
|
|
7955
|
+
if (!propsObjName) return false
|
|
7956
|
+
const parsed = constantsByName(ctx).get(name)?.parsed
|
|
7957
|
+
return !!parsed && parsed.kind === 'member' && !parsed.computed && parsed.object.kind === 'identifier' && parsed.object.name === propsObjName
|
|
7958
|
+
}
|
|
7959
|
+
|
|
7960
|
+
/**
|
|
7961
|
+
* `constantValuesByName(ctx)`, narrowed to the constants an alias-hop walk
|
|
7962
|
+
* may legitimately continue into when resolving whether a loop's array (or
|
|
7963
|
+
* a property access's object) ultimately denotes a prop (#2724 review
|
|
7964
|
+
* finding — a second pass after the initial fix landed found two false
|
|
7965
|
+
* positives the unrestricted map let through):
|
|
7966
|
+
*
|
|
7967
|
+
* - Never a MODULE-scope constant (`isModule`) — a module constant cannot,
|
|
7968
|
+
* by definition, be an alias of any one component instance's props, so
|
|
7969
|
+
* hopping into one can only ever be a coincidental name match. Same
|
|
7970
|
+
* exclusion `resolveGetterAliases` (csr-substitute.ts, #2778) already
|
|
7971
|
+
* makes for its own alias-hop walk.
|
|
7972
|
+
* - Never a name currently SHADOWED by an active loop/callback binding
|
|
7973
|
+
* (`ctx.scope.isBound`) — `rows.map((list) => list.map(...))`'s inner
|
|
7974
|
+
* `list` must resolve to the OUTER LOOP'S ROW VALUE, never an unrelated
|
|
7975
|
+
* outer-scope `const list = someProp` the row happens to shadow. Same
|
|
7976
|
+
* shadow-guard shape as `tryResolveTemplateSpanFromConst`'s guard
|
|
7977
|
+
* (#2222-family) — `isBound()` (all binding sources), not
|
|
7978
|
+
* `valueBoundNames()`: this is that guard's consumer class, not the
|
|
7979
|
+
* reactivity/slot-ID classifier one (`BindingScope.valueBoundNames`'s
|
|
7980
|
+
* docstring, #2482 Stage 1a Commit 2). This map only half-delivers that
|
|
7981
|
+
* guarantee — it stops `resolveAliasOrigin` from hopping INTO a shadowed
|
|
7982
|
+
* name's value, but the FIRST call (`name` itself, before any hop) goes
|
|
7983
|
+
* straight to the terminal (`isDirectPropBindingName`), which has its own
|
|
7984
|
+
* matching `ctx.scope.isBound` check for exactly this reason (pullfrog
|
|
7985
|
+
* review finding on this PR — a shadowed outer const whose OWN value is a
|
|
7986
|
+
* member-access-on-props shape resolved as prop-derived on the very first
|
|
7987
|
+
* hop, before this map's filter ever applied).
|
|
7988
|
+
*
|
|
7989
|
+
* `let` is deliberately NOT excluded, despite being reassignable — this was
|
|
7990
|
+
* tried during review and reverted (second review pass): `isStaticArray`
|
|
7991
|
+
* (jsx-to-ir.ts's derivation) treats "not recognized as prop-derived" as
|
|
7992
|
+
* STATIC, not dynamic, so excluding `let` doesn't move a `let`-aliased prop
|
|
7993
|
+
* array to the safe/over-reconciling side — it reproduces #2724's exact
|
|
7994
|
+
* silent-divergence shape (the array silently stays on the static
|
|
7995
|
+
* `qsaChildScopes` path, so a CSR-only mount never wires up the row's
|
|
7996
|
+
* child). A `let` reassigned before use could in principle make this hop
|
|
7997
|
+
* resolve against a stale declaration-time value — but that only risks the
|
|
7998
|
+
* SAFE direction (an extra `mapArray` on a loop that turns out not to be
|
|
7999
|
+
* prop-derived after all), matching this file's existing `hasCalls`
|
|
8000
|
+
* over-reconciliation-is-cheap philosophy, and Go's `renderLoop` BF101 gate
|
|
8001
|
+
* independently refuses any bare-identifier loop array that resolves to a
|
|
8002
|
+
* non-module local variable (`let` or `const` alike) before
|
|
8003
|
+
* `isPropDerivedArray` would ever drive real Go codegen off it.
|
|
8004
|
+
*
|
|
8005
|
+
* A NARROWED VIEW, not a change to `constantValuesByName` itself — that
|
|
8006
|
+
* function is also `forwardsCallerRestProps`'s rest/props-spread alias
|
|
8007
|
+
* walk, whose existing (wider) hop set this deliberately leaves alone.
|
|
8008
|
+
*/
|
|
8009
|
+
function propAliasHopCandidates(ctx: TransformContext): ReadonlyMap<string, string | undefined> {
|
|
8010
|
+
const bound = ctx.scope.boundNames()
|
|
8011
|
+
const candidates = new Map<string, string | undefined>()
|
|
8012
|
+
for (const constant of ctx.analyzer.localConstants) {
|
|
8013
|
+
if (constant.isModule || bound.has(constant.name)) continue
|
|
8014
|
+
if (!candidates.has(constant.name)) candidates.set(constant.name, constant.value)
|
|
8015
|
+
}
|
|
8016
|
+
return candidates
|
|
8017
|
+
}
|
|
8018
|
+
|
|
7872
8019
|
/**
|
|
7873
8020
|
* Check if array expression is a signal or memo getter call.
|
|
7874
8021
|
* Used to determine if a loop needs mapArray for dynamic DOM updates.
|
|
@@ -8046,7 +8193,7 @@ function isPropsReference(expr: string, ctx: TransformContext, visited?: Set<str
|
|
|
8046
8193
|
function forwardsCallerRestProps(attrs: IRAttribute[], ctx: TransformContext): boolean {
|
|
8047
8194
|
for (const attr of attrs) {
|
|
8048
8195
|
if (attr.value.kind !== 'spread') continue
|
|
8049
|
-
const constants =
|
|
8196
|
+
const constants = constantValuesByName(ctx)
|
|
8050
8197
|
for (const name of new Set([attr.value.expr, attr.value.templateExpr])) {
|
|
8051
8198
|
if (!name) continue
|
|
8052
8199
|
if (resolveRestSpreadOriginCore(ctx.analyzer, constants, name) !== null) return true
|
|
@@ -8055,14 +8202,25 @@ function forwardsCallerRestProps(attrs: IRAttribute[], ctx: TransformContext): b
|
|
|
8055
8202
|
return false
|
|
8056
8203
|
}
|
|
8057
8204
|
|
|
8058
|
-
/** `ctx.analyzer.localConstants` indexed by name, memoized per component walk. */
|
|
8059
|
-
function
|
|
8060
|
-
if (ctx.
|
|
8205
|
+
/** `ctx.analyzer.localConstants` indexed by name → value text, memoized per component walk. */
|
|
8206
|
+
function constantValuesByName(ctx: TransformContext): ReadonlyMap<string, string | undefined> {
|
|
8207
|
+
if (ctx._constantValuesByName) return ctx._constantValuesByName
|
|
8061
8208
|
const byName = new Map<string, string | undefined>()
|
|
8062
8209
|
for (const constant of ctx.analyzer.localConstants) {
|
|
8063
8210
|
if (!byName.has(constant.name)) byName.set(constant.name, constant.value)
|
|
8064
8211
|
}
|
|
8065
|
-
ctx.
|
|
8212
|
+
ctx._constantValuesByName = byName
|
|
8213
|
+
return byName
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
/** `ctx.analyzer.localConstants` indexed by name → full `ConstantInfo`, memoized per component walk. */
|
|
8217
|
+
function constantsByName(ctx: TransformContext): ReadonlyMap<string, ConstantInfo> {
|
|
8218
|
+
if (ctx._constantsByName) return ctx._constantsByName
|
|
8219
|
+
const byName = new Map<string, ConstantInfo>()
|
|
8220
|
+
for (const constant of ctx.analyzer.localConstants) {
|
|
8221
|
+
if (!byName.has(constant.name)) byName.set(constant.name, constant)
|
|
8222
|
+
}
|
|
8223
|
+
ctx._constantsByName = byName
|
|
8066
8224
|
return byName
|
|
8067
8225
|
}
|
|
8068
8226
|
|
package/src/lowering-registry.ts
CHANGED
|
@@ -141,6 +141,27 @@ export function matchLoweringCall(
|
|
|
141
141
|
return null
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* The child expressions a plugin took responsibility for inside a matched
|
|
146
|
+
* call (#2843) — the base + every triple's value/guard for a `guard-list`,
|
|
147
|
+
* or the args for a `helper-call`. Shared by the support gate
|
|
148
|
+
* (`checkSupport`'s `call` arm in `expression-parser.ts`, which recurses
|
|
149
|
+
* into these instead of refusing the call's raw argument shapes — a
|
|
150
|
+
* `guard-list`'s params object literal, for instance, would otherwise be
|
|
151
|
+
* refused before any adapter ever sees the recognised node) and any future
|
|
152
|
+
* walker that needs the same "what did this plugin actually consume"
|
|
153
|
+
* answer, so the two can't drift on what counts as consumed.
|
|
154
|
+
*/
|
|
155
|
+
export function loweringNodeChildren(node: LoweringNode): ParsedExpr[] {
|
|
156
|
+
if (node.kind === 'helper-call') return [...node.args]
|
|
157
|
+
const children: ParsedExpr[] = [node.base]
|
|
158
|
+
for (const t of node.triples) {
|
|
159
|
+
if (t.guard) children.push(t.guard)
|
|
160
|
+
children.push(t.value)
|
|
161
|
+
}
|
|
162
|
+
return children
|
|
163
|
+
}
|
|
164
|
+
|
|
144
165
|
/**
|
|
145
166
|
* Whether a `LoweringNode.helper` id is safe to splice directly into an
|
|
146
167
|
* adapter's runtime-helper naming convention (`bf_${helper}` in Go,
|
package/src/prop-rewrite.ts
CHANGED
|
@@ -132,6 +132,16 @@ export function collectAstPropRefs(
|
|
|
132
132
|
* (`{ org }` → `{ org: _p.org }`) so the result stays syntactically
|
|
133
133
|
* valid.
|
|
134
134
|
*
|
|
135
|
+
* `replacementFor` overrides the substitution text for a value
|
|
136
|
+
* reference (default `${PROPS_PARAM}.<callerKey>`) — e.g. the reactive
|
|
137
|
+
* client-JS emit path (`emit-reactive.ts`'s `rewriteDestructuredPropsInExpr`)
|
|
138
|
+
* wraps it in a `(_p.x ?? <default>)` fallback for a prop with a
|
|
139
|
+
* destructure default. Kept as a caller override rather than a
|
|
140
|
+
* duplicate AST walk in that module, per the "one decision, one
|
|
141
|
+
* implementation" rule (CLAUDE.md) — this walk is already the one place
|
|
142
|
+
* that correctly distinguishes a value reference from an object-literal
|
|
143
|
+
* key / property-access name / shorthand property / binding name.
|
|
144
|
+
*
|
|
135
145
|
* Returns null when `text` does not parse cleanly as an expression —
|
|
136
146
|
* the caller falls back to the legacy regex rewrite.
|
|
137
147
|
*/
|
|
@@ -139,6 +149,7 @@ function applyScopedPropRefRewrite(
|
|
|
139
149
|
text: string,
|
|
140
150
|
propRefs: Set<string>,
|
|
141
151
|
propAliases?: ReadonlyMap<string, string>,
|
|
152
|
+
replacementFor?: (localName: string, callerKey: string) => string,
|
|
142
153
|
): string | null {
|
|
143
154
|
// Wrap in parens so object literals and arrows parse as expressions.
|
|
144
155
|
const prefix = '('
|
|
@@ -157,11 +168,12 @@ function applyScopedPropRefRewrite(
|
|
|
157
168
|
// — #2524 CSR half); the local binding (`n.text`) only survives on the
|
|
158
169
|
// left of a shorthand expansion.
|
|
159
170
|
const callerKey = propAliases?.get(n.text) ?? n.text
|
|
171
|
+
const value = replacementFor ? replacementFor(n.text, callerKey) : `${PROPS_PARAM}.${callerKey}`
|
|
160
172
|
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === n) {
|
|
161
|
-
edits.push({ start, end, replacement: `${n.text}: ${
|
|
173
|
+
edits.push({ start, end, replacement: `${n.text}: ${value}` })
|
|
162
174
|
return
|
|
163
175
|
}
|
|
164
|
-
edits.push({ start, end, replacement:
|
|
176
|
+
edits.push({ start, end, replacement: value })
|
|
165
177
|
})
|
|
166
178
|
|
|
167
179
|
if (edits.length === 0) return text
|
|
@@ -186,6 +198,7 @@ export function applyRegexPropRefRewrite(
|
|
|
186
198
|
text: string,
|
|
187
199
|
propRefs: Iterable<string>,
|
|
188
200
|
propAliases?: ReadonlyMap<string, string>,
|
|
201
|
+
replacementFor?: (localName: string, callerKey: string) => string,
|
|
189
202
|
): string {
|
|
190
203
|
const { protect, restore } = createTemplateAwareStringProtector()
|
|
191
204
|
let result = protect(text)
|
|
@@ -193,6 +206,7 @@ export function applyRegexPropRefRewrite(
|
|
|
193
206
|
for (const propName of propRefs) {
|
|
194
207
|
// `_p` is always keyed by the caller-facing name (#2524 CSR half).
|
|
195
208
|
const callerKey = propAliases?.get(propName) ?? propName
|
|
209
|
+
const value = replacementFor ? replacementFor(propName, callerKey) : `${PROPS_PARAM}.${callerKey}`
|
|
196
210
|
const pattern = new RegExp(`(?<!${PROPS_PARAM}\\.)(?<!['"\\w.-])\\b${propName}\\b(?![a-zA-Z0-9_$])`, 'g')
|
|
197
211
|
result = result.replace(pattern, (match, offset, str) => {
|
|
198
212
|
// Skip object literal keys: preceded by { or , and followed by :
|
|
@@ -201,7 +215,7 @@ export function applyRegexPropRefRewrite(
|
|
|
201
215
|
const before = str.slice(0, offset)
|
|
202
216
|
if (/[{,]\s*$/.test(before)) return match
|
|
203
217
|
}
|
|
204
|
-
return
|
|
218
|
+
return value
|
|
205
219
|
})
|
|
206
220
|
}
|
|
207
221
|
|
|
@@ -225,6 +239,9 @@ export function applyRegexPropRefRewrite(
|
|
|
225
239
|
* `_p` is always keyed by the caller-facing name (#2524 CSR half); a name
|
|
226
240
|
* absent from this map emits `_p.<name>` unchanged (the un-aliased case,
|
|
227
241
|
* where `sourceName ?? name` is an identity).
|
|
242
|
+
* @param replacementFor - Override the substitution text for a value
|
|
243
|
+
* reference (default `${PROPS_PARAM}.<callerKey>`) — see
|
|
244
|
+
* `applyScopedPropRefRewrite`'s docstring.
|
|
228
245
|
*/
|
|
229
246
|
export function rewriteBarePropRefs(
|
|
230
247
|
text: string,
|
|
@@ -232,6 +249,7 @@ export function rewriteBarePropRefs(
|
|
|
232
249
|
propNames: Set<string>,
|
|
233
250
|
extraPropRefs?: ReadonlySet<string>,
|
|
234
251
|
propAliases?: ReadonlyMap<string, string>,
|
|
252
|
+
replacementFor?: (localName: string, callerKey: string) => string,
|
|
235
253
|
): string | undefined {
|
|
236
254
|
// Walk AST to find which prop names are actually used as value references
|
|
237
255
|
const foundPropRefs = new Set<string>()
|
|
@@ -243,7 +261,7 @@ export function rewriteBarePropRefs(
|
|
|
243
261
|
}
|
|
244
262
|
if (foundPropRefs.size === 0) return undefined
|
|
245
263
|
return (
|
|
246
|
-
applyScopedPropRefRewrite(text, foundPropRefs, propAliases) ??
|
|
247
|
-
applyRegexPropRefRewrite(text, foundPropRefs, propAliases)
|
|
264
|
+
applyScopedPropRefRewrite(text, foundPropRefs, propAliases, replacementFor) ??
|
|
265
|
+
applyRegexPropRefRewrite(text, foundPropRefs, propAliases, replacementFor)
|
|
248
266
|
)
|
|
249
267
|
}
|
package/src/props-binding.ts
CHANGED
|
@@ -69,6 +69,31 @@ export function buildPropAliasMap(params: readonly ParamInfo[]): Map<string, str
|
|
|
69
69
|
return map
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The props-parameter shape needed to answer "is `name` an actual local
|
|
74
|
+
* binding introduced by the props parameter" — as opposed to
|
|
75
|
+
* `ctx.patterns.props`, which for a whole `(props: Props)` parameter holds
|
|
76
|
+
* every TYPE-MEMBER name (`extractPropsFromTypeMembers`) for regex prop-
|
|
77
|
+
* access matching, not real bindings.
|
|
78
|
+
*/
|
|
79
|
+
export interface PropsParamBindings {
|
|
80
|
+
propsParams: readonly ParamInfo[]
|
|
81
|
+
propsObjectName: string | null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Local names actually bound by the props parameter. Empty for a whole
|
|
86
|
+
* `(props: Props)` parameter: `propsParams` there is type-member names
|
|
87
|
+
* (nothing local to shadow with), and `props` itself is `propsObjectName`,
|
|
88
|
+
* checked separately by callers.
|
|
89
|
+
*/
|
|
90
|
+
export function boundPropLocalNames(b: PropsParamBindings): ReadonlySet<string> {
|
|
91
|
+
if (b.propsObjectName !== null) return EMPTY_SET
|
|
92
|
+
return new Set(b.propsParams.filter(p => !p.isRest).map(p => p.name))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const EMPTY_SET: ReadonlySet<string> = new Set()
|
|
96
|
+
|
|
72
97
|
/**
|
|
73
98
|
* The two component bindings that forward the caller's leftover props:
|
|
74
99
|
* the destructured `...rest` binding and a whole undestructured `(props)`
|
|
@@ -82,10 +107,52 @@ export interface RestSpreadBindings {
|
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
110
|
+
* Walks a bare `const x__alias = <name>` hop chain recorded in
|
|
111
|
+
* `constantValues` (name → initializer text) until `terminal` recognizes
|
|
112
|
+
* the current name, returning what it recognized. `null` when the chain
|
|
113
|
+
* runs out (a constant whose value isn't a bare identifier — a real
|
|
114
|
+
* computed object — stops the walk) or degenerates into a cycle.
|
|
115
|
+
*
|
|
116
|
+
* The one alias-hop walker in the compiler, shared by every caller that
|
|
117
|
+
* needs "does this name ultimately reach some fixed binding" rather than
|
|
118
|
+
* a bespoke copy per caller — `resolveRestSpreadOriginCore` below,
|
|
119
|
+
* `resolveGetterAliases` (`csr-substitute.ts`, #2778), and
|
|
120
|
+
* `isArrayExprDirectPropRef` (`jsx-to-ir.ts`, #2724, called directly rather
|
|
121
|
+
* than through a `*Core`-style wrapper) are its callers, differing only in
|
|
122
|
+
* what `terminal` recognizes as a hit — and, for `isArrayExprDirectPropRef`,
|
|
123
|
+
* in also narrowing `constantValues` itself (`propAliasHopCandidates`)
|
|
124
|
+
* before passing it in. `terminal` is checked BEFORE the constant-value
|
|
125
|
+
* lookup so a name that is itself a target take priority over any
|
|
126
|
+
* same-named local shadowing it (preserves `resolveRestSpreadOriginCore`'s
|
|
127
|
+
* original hop order) — a caller whose `terminal` can itself be fooled by a
|
|
128
|
+
* shadowed name (e.g. a scope-unaware lookup keyed only by name) must guard
|
|
129
|
+
* that inside `terminal`, since this walker has no scope/binding awareness
|
|
130
|
+
* of its own to do it centrally.
|
|
131
|
+
*
|
|
132
|
+
* `visited` guards a constant cycle (`const a = b; const b = a`); walking
|
|
133
|
+
* hop by hop (not a precomputed set) is what lets a multi-hop alias
|
|
134
|
+
* (`const p2 = props; const p3 = p2`) resolve through every link.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveAliasOrigin<T>(
|
|
137
|
+
constantValues: ReadonlyMap<string, string | undefined>,
|
|
138
|
+
name: string,
|
|
139
|
+
terminal: (name: string) => T | null,
|
|
140
|
+
): T | null {
|
|
141
|
+
const visited = new Set<string>()
|
|
142
|
+
let current: string | undefined = name.trim()
|
|
143
|
+
while (current !== undefined && !visited.has(current)) {
|
|
144
|
+
const hit = terminal(current)
|
|
145
|
+
if (hit !== null) return hit
|
|
146
|
+
visited.add(current)
|
|
147
|
+
current = constantValues.get(current)?.trim()
|
|
148
|
+
}
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Which of `bindings` the name `name` ultimately reaches. `'rest'` for the
|
|
154
|
+
* destructured rest binding, `'props'` for the whole props object, `null`
|
|
155
|
+
* for anything else.
|
|
89
156
|
*
|
|
90
157
|
* The single definition of "this `{...spread}` forwards the caller's
|
|
91
158
|
* leftover props", shared by the two phases that must agree on it: Phase 1
|
|
@@ -96,26 +163,15 @@ export interface RestSpreadBindings {
|
|
|
96
163
|
* `spreadAttrs({...})` merge. Two copies of the rule would let an element
|
|
97
164
|
* qualify for one and not the other, which is exactly the silent-drop
|
|
98
165
|
* shape #2754 reports.
|
|
99
|
-
*
|
|
100
|
-
* Walks hop by hop (not a precomputed set) so a multi-hop alias
|
|
101
|
-
* (`const p2 = props; const p3 = p2`) resolves through every link;
|
|
102
|
-
* `visited` guards a constant cycle (`const a = b; const b = a`). A
|
|
103
|
-
* constant whose value isn't a bare identifier on the chain (a real
|
|
104
|
-
* computed object) stops the walk, so a genuinely different spread
|
|
105
|
-
* expression is never mistaken for the rest object.
|
|
106
166
|
*/
|
|
107
167
|
export function resolveRestSpreadOriginCore(
|
|
108
168
|
bindings: RestSpreadBindings,
|
|
109
169
|
constantValues: ReadonlyMap<string, string | undefined>,
|
|
110
170
|
name: string,
|
|
111
171
|
): 'rest' | 'props' | null {
|
|
112
|
-
|
|
113
|
-
let current: string | undefined = name.trim()
|
|
114
|
-
while (current !== undefined && !visited.has(current)) {
|
|
172
|
+
return resolveAliasOrigin(constantValues, name, (current) => {
|
|
115
173
|
if (bindings.restPropsName && current === bindings.restPropsName) return 'rest'
|
|
116
174
|
if (bindings.propsObjectName && current === bindings.propsObjectName) return 'props'
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
return null
|
|
175
|
+
return null
|
|
176
|
+
})
|
|
121
177
|
}
|
package/src/types.ts
CHANGED
|
@@ -594,7 +594,9 @@ export interface IRLoop {
|
|
|
594
594
|
isStaticArray: boolean
|
|
595
595
|
|
|
596
596
|
/**
|
|
597
|
-
* True when the array is a direct prop reference promoted to mapArray
|
|
597
|
+
* True when the array is a direct prop reference promoted to mapArray
|
|
598
|
+
* (#1586) — including one reached only through a bare `const x = y`
|
|
599
|
+
* alias-hop chain (`isArrayExprDirectPropRef`, jsx-to-ir.ts, #2724).
|
|
598
600
|
* Adapters use this to include the array in JSON serialization (bf-p)
|
|
599
601
|
* even though isStaticArray is false — the client's mapArray needs the
|
|
600
602
|
* initial data for hydration, unlike signal-backed arrays.
|
|
@@ -1548,10 +1550,9 @@ export interface SignalInfo {
|
|
|
1548
1550
|
* `templateCondition` fields on other IR positions. Destructured mode
|
|
1549
1551
|
* only (`propsObjectName === null`); undefined when no rewrite was
|
|
1550
1552
|
* needed (no destructured prop referenced) or the component is in
|
|
1551
|
-
* object-props mode (there, `rewritePropsObjectRef
|
|
1552
|
-
*
|
|
1553
|
-
*
|
|
1554
|
-
* AST-level pre-rewrite).
|
|
1553
|
+
* object-props mode (there, `rewritePropsObjectRef` handles `props.x` →
|
|
1554
|
+
* `_p.x` on the final joined/substituted string instead, since a bare
|
|
1555
|
+
* `props.x` textual match works without an AST-level pre-rewrite).
|
|
1555
1556
|
*/
|
|
1556
1557
|
templateInitialValue?: string
|
|
1557
1558
|
/**
|