@barefootjs/jsx 0.33.3 → 0.33.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/child-scope.d.ts +13 -0
- package/dist/adapters/child-scope.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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1392 -1117
- package/dist/ir-to-client-js/build-references.d.ts.map +1 -1
- 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/plan/build-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +2 -4
- package/dist/ir-to-client-js/control-flow/plan/loop.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/component-loop.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/control-flow.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/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 +19 -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__/fragment-body-loop-key.test.ts +95 -0
- 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__/markup-prop-brand.test.ts +165 -26
- package/src/__tests__/multi-root-loop-body.test.ts +7 -3
- package/src/__tests__/namespace-import-primitive.test.ts +244 -0
- package/src/__tests__/preamble-declarations.test.ts +42 -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/analyzer.ts +130 -0
- package/src/errors.ts +14 -1
- package/src/index.ts +7 -0
- package/src/ir-to-client-js/build-references.ts +7 -0
- 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/plan/build-component-loop.ts +19 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +2 -4
- package/src/ir-to-client-js/control-flow/shared.ts +12 -5
- package/src/ir-to-client-js/control-flow/stringify/component-loop.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +8 -2
- package/src/ir-to-client-js/control-flow.ts +12 -0
- 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 +194 -82
- 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 +318 -54
- package/src/prop-rewrite.ts +23 -5
- package/src/props-binding.ts +74 -18
- package/src/types.ts +19 -5
|
@@ -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
|
}
|
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,
|
|
@@ -3793,6 +3817,19 @@ function extractLoopKey(node: IRNode): string | null {
|
|
|
3793
3817
|
if (b === null) return null
|
|
3794
3818
|
return normalizeKeyExpr(a) === normalizeKeyExpr(b) ? a : null
|
|
3795
3819
|
}
|
|
3820
|
+
if (node.type === 'fragment') {
|
|
3821
|
+
// #2763: a `.map()` body returning a fragment (`<><li key={..}/><li/></>`)
|
|
3822
|
+
// reads the key off its FIRST element, not first node — same "first
|
|
3823
|
+
// element, not first node" rule as `IRElement.keyAttr`'s docstring and
|
|
3824
|
+
// `markCarrierIn`'s relay case, skipping whitespace-only text the way
|
|
3825
|
+
// `branchHasNoElement` does. Recursing through `extractLoopKey` (rather
|
|
3826
|
+
// than requiring `type === 'element'` directly) lets a nested
|
|
3827
|
+
// conditional/fragment first child resolve too.
|
|
3828
|
+
const first = node.children.find(
|
|
3829
|
+
(c) => !(c.type === 'text' && typeof c.value === 'string' && !c.value.trim())
|
|
3830
|
+
)
|
|
3831
|
+
return first ? extractLoopKey(first) : null
|
|
3832
|
+
}
|
|
3796
3833
|
return null
|
|
3797
3834
|
}
|
|
3798
3835
|
|
|
@@ -4108,7 +4145,7 @@ function leafIsWirelessElement(el: ts.JsxElement | ts.JsxSelfClosingElement): bo
|
|
|
4108
4145
|
if (ts.isJsxSpreadAttribute(attr)) { ok = false; return }
|
|
4109
4146
|
if (ts.isJsxAttribute(attr)) {
|
|
4110
4147
|
const name = attr.name.getText()
|
|
4111
|
-
if (
|
|
4148
|
+
if (isEventHandlerAttrName(name)) { ok = false; return }
|
|
4112
4149
|
}
|
|
4113
4150
|
}
|
|
4114
4151
|
}
|
|
@@ -4206,12 +4243,12 @@ function tagLoopItemRootComponents(nodes: readonly IRNode[]): void {
|
|
|
4206
4243
|
*
|
|
4207
4244
|
* Deliberately does NOT remove the literal `key` entry from `.attrs`: the
|
|
4208
4245
|
* CSR/client-JS codegen path (`ir-to-client-js/html-template.ts`'s
|
|
4209
|
-
* `
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
4213
|
-
* skip the raw `key` attr in their generic `renderAttributes`
|
|
4214
|
-
* (Hono's own JSX runtime already drops it silently either way).
|
|
4246
|
+
* `resolvedKeyAttrName` helper, #2763) reads it back off `.attrs` too, but
|
|
4247
|
+
* only emits `data-key` when THIS field — `node.keyAttr` — is actually set;
|
|
4248
|
+
* a raw `key` attribute the SSR side didn't recognize as a row key (e.g. a
|
|
4249
|
+
* fragment-bodied row before #2763's fix) no longer gets baked client-side
|
|
4250
|
+
* either. Adapters skip the raw `key` attr in their generic `renderAttributes`
|
|
4251
|
+
* loop instead (Hono's own JSX runtime already drops it silently either way).
|
|
4215
4252
|
*
|
|
4216
4253
|
* `component` bodies are left untouched: a `<Comp key={x}/>` row root's key
|
|
4217
4254
|
* is a PROP (`renderComponentProps`'s own `data-key` prop-forwarding), not
|
|
@@ -4226,6 +4263,16 @@ function applyLoopKeyAttr(node: IRNode, name: string, value: string): void {
|
|
|
4226
4263
|
if (node.type === 'conditional') {
|
|
4227
4264
|
applyLoopKeyAttr(node.whenTrue, name, value)
|
|
4228
4265
|
applyLoopKeyAttr(node.whenFalse, name, value)
|
|
4266
|
+
return
|
|
4267
|
+
}
|
|
4268
|
+
if (node.type === 'fragment') {
|
|
4269
|
+
// Write-side twin of `extractLoopKey`'s fragment case: stamp the SAME
|
|
4270
|
+
// first-real-child this key was read from, not every child, matching
|
|
4271
|
+
// the "first element, not first node" rule.
|
|
4272
|
+
const first = node.children.find(
|
|
4273
|
+
(c) => !(c.type === 'text' && typeof c.value === 'string' && !c.value.trim())
|
|
4274
|
+
)
|
|
4275
|
+
if (first) applyLoopKeyAttr(first, name, value)
|
|
4229
4276
|
}
|
|
4230
4277
|
}
|
|
4231
4278
|
|
|
@@ -4667,7 +4714,7 @@ function transformMapCall(
|
|
|
4667
4714
|
// the conditional — the same carrier the single-return path uses. A
|
|
4668
4715
|
// JS-runtime adapter (and the browser under /* @client */) runs it
|
|
4669
4716
|
// verbatim.
|
|
4670
|
-
preamble = preambleFromValueStatements(pre, ctx)
|
|
4717
|
+
preamble = preambleFromValueStatements(pre, ctx, body.statements)
|
|
4671
4718
|
|
|
4672
4719
|
// Stage 2 of spec/callback-fidelity.md, narrowed by #2447. This used
|
|
4673
4720
|
// to refuse EVERY branch preamble on a DSL target, on the premise
|
|
@@ -4874,7 +4921,7 @@ function transformMapCall(
|
|
|
4874
4921
|
valueStmts.push(stmt)
|
|
4875
4922
|
}
|
|
4876
4923
|
if (valueStmts.length > 0) {
|
|
4877
|
-
preamble = preambleFromValueStatements(valueStmts, ctx)
|
|
4924
|
+
preamble = preambleFromValueStatements(valueStmts, ctx, body.statements)
|
|
4878
4925
|
|
|
4879
4926
|
// #2447. A DSL adapter cannot execute `preamble.segments` (JS
|
|
4880
4927
|
// text), so it lowers `declarations` — one per-row local each —
|
|
@@ -5456,8 +5503,8 @@ function buildFlatMapCallback(
|
|
|
5456
5503
|
// Template variant per segment, like buildPreambleSegments: the hydrate
|
|
5457
5504
|
// registration template is module scope (`template: (_p) => ...`), so a
|
|
5458
5505
|
// bare destructured-prop reference must rewrite to `_p.xxx` there —
|
|
5459
|
-
// `
|
|
5460
|
-
// object name).
|
|
5506
|
+
// `rewritePropsObjectRef` can't do it (destructured components have no
|
|
5507
|
+
// props object name to rewrite from).
|
|
5461
5508
|
const tpl = rewriteBarePropRefs(piece.js, body, ctx)
|
|
5462
5509
|
return tpl !== undefined && tpl !== piece.js
|
|
5463
5510
|
? { kind: 'js', text: piece.js, templateText: tpl }
|
|
@@ -5844,6 +5891,7 @@ function computePreambleReactiveNames(
|
|
|
5844
5891
|
function preambleFromValueStatements(
|
|
5845
5892
|
statements: readonly ts.Statement[],
|
|
5846
5893
|
ctx: TransformContext,
|
|
5894
|
+
bodyStatements: readonly ts.Statement[],
|
|
5847
5895
|
): MapCallbackPreamble {
|
|
5848
5896
|
const segments: PreambleSegment[] = []
|
|
5849
5897
|
const typedParts: string[] = []
|
|
@@ -5865,7 +5913,7 @@ function preambleFromValueStatements(
|
|
|
5865
5913
|
declaredNames: [...declared],
|
|
5866
5914
|
// Value-only preambles accumulate no JSX, so no child needs the array join.
|
|
5867
5915
|
builderNames: [],
|
|
5868
|
-
declarations: neutralPreambleDeclarations(statements, ctx) ?? undefined,
|
|
5916
|
+
declarations: neutralPreambleDeclarations(statements, ctx, bodyStatements) ?? undefined,
|
|
5869
5917
|
reactiveNames: reactiveNames.size > 0 ? [...reactiveNames] : undefined,
|
|
5870
5918
|
}
|
|
5871
5919
|
}
|
|
@@ -5882,6 +5930,7 @@ function preambleFromValueStatements(
|
|
|
5882
5930
|
function neutralPreambleDeclarations(
|
|
5883
5931
|
statements: readonly ts.Statement[],
|
|
5884
5932
|
ctx: TransformContext,
|
|
5933
|
+
bodyStatements: readonly ts.Statement[],
|
|
5885
5934
|
): PreambleValueDeclaration[] | null {
|
|
5886
5935
|
const out: PreambleValueDeclaration[] = []
|
|
5887
5936
|
for (const stmt of statements) {
|
|
@@ -5893,6 +5942,20 @@ function neutralPreambleDeclarations(
|
|
|
5893
5942
|
// no adapter has a single per-row local form for that.
|
|
5894
5943
|
if (!ts.isIdentifier(decl.name)) return null
|
|
5895
5944
|
if (!decl.initializer) return null
|
|
5945
|
+
// #2797: a function-valued local (an event handler hoisted out of the
|
|
5946
|
+
// JSX attribute position, e.g. `const handleClick = () => {...}`) has
|
|
5947
|
+
// no template-expression form on any backend — no DSL here has a
|
|
5948
|
+
// callable value. But every adapter's own SSR component-prop builder
|
|
5949
|
+
// already skips an event-handler prop (`isEventHandlerAttrName`)
|
|
5950
|
+
// outright, so a name read ONLY that way needs no SSR representation
|
|
5951
|
+
// at all — elide the declaration instead of refusing the whole
|
|
5952
|
+
// preamble. The client-JS preamble (`segments`, not `declarations`)
|
|
5953
|
+
// still carries the real closure; this only concerns what the DSL
|
|
5954
|
+
// SSR template sees.
|
|
5955
|
+
if (isFunctionLiteral(decl.initializer)) {
|
|
5956
|
+
if (everyReadIsEventHandlerPropValue(decl.name.text, bodyStatements)) continue
|
|
5957
|
+
return null
|
|
5958
|
+
}
|
|
5896
5959
|
const valueParsed = tsNodeToParsedExpr(decl.initializer)
|
|
5897
5960
|
// The same subset gate every other DSL-lowered expression passes
|
|
5898
5961
|
// through. An adapter that additionally cannot emit a supported shape
|
|
@@ -5907,7 +5970,74 @@ function neutralPreambleDeclarations(
|
|
|
5907
5970
|
})
|
|
5908
5971
|
}
|
|
5909
5972
|
}
|
|
5910
|
-
|
|
5973
|
+
// Not `out.length > 0 ? out : null` — an all-elided preamble legitimately
|
|
5974
|
+
// has zero declarations to emit (see MapCallbackPreamble.declarations'
|
|
5975
|
+
// docstring); `[]` is what tells the Phase-1 gate "fully declared."
|
|
5976
|
+
return out
|
|
5977
|
+
}
|
|
5978
|
+
|
|
5979
|
+
/** `const x = () => {...}` / `const x = function () {...}` — a callable value, never a template-expression shape. */
|
|
5980
|
+
function isFunctionLiteral(node: ts.Expression): boolean {
|
|
5981
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node)
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
/**
|
|
5985
|
+
* Whether every read of `name` in `bodyStatements`, other than its own
|
|
5986
|
+
* binding site, sits inside an event-handler `JsxAttribute`'s
|
|
5987
|
+
* (`onClick`/`onInput`/...) value — i.e. `name={<ident>}` where the
|
|
5988
|
+
* attribute's own name passes {@link isEventHandlerAttrName}, the exact
|
|
5989
|
+
* predicate every DSL adapter's own SSR prop-builder already uses to skip an
|
|
5990
|
+
* event-handler prop (e.g. `packages/adapter-jinja/src/adapter/
|
|
5991
|
+
* jinja-adapter.ts`'s `renderComponent`, `packages/adapter-go-template/...`'s
|
|
5992
|
+
* `prop.isEventHandler` check).
|
|
5993
|
+
*
|
|
5994
|
+
* Walks the raw preamble+return syntax rather than the already-built row IR
|
|
5995
|
+
* (`children`) deliberately: `neutralPreambleDeclarations` runs on the
|
|
5996
|
+
* PREAMBLE statements specifically, and the IR built from the return
|
|
5997
|
+
* expression doesn't retain a per-occurrence link back to which SOURCE
|
|
5998
|
+
* identifier fed a given `IREvent`/`IRProp` — re-deriving from syntax here
|
|
5999
|
+
* keeps the safety check self-contained rather than threading that link
|
|
6000
|
+
* through the IR builder for one caller.
|
|
6001
|
+
*
|
|
6002
|
+
* Conservative: a property-access name (`x.name`), a nested declaration's own
|
|
6003
|
+
* binding, or any other read shape (a text/attr expression, a NON-event
|
|
6004
|
+
* child prop, a call) fails this and falls through to the ordinary refusal —
|
|
6005
|
+
* this only widens the shapes that are SAFE to elide, never narrows what was
|
|
6006
|
+
* already accepted. Uses `ts.forEachChild`'s own short-circuit (a truthy
|
|
6007
|
+
* return stops the walk) rather than a separate mutable flag.
|
|
6008
|
+
*/
|
|
6009
|
+
function everyReadIsEventHandlerPropValue(name: string, bodyStatements: readonly ts.Statement[]): boolean {
|
|
6010
|
+
const findDisallowedRead = (node: ts.Node): true | undefined => {
|
|
6011
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
6012
|
+
const isBindingName = ts.isVariableDeclaration(node.parent) && node.parent.name === node
|
|
6013
|
+
const isPropertyName = ts.isPropertyAccessExpression(node.parent) && node.parent.name === node
|
|
6014
|
+
if (!isBindingName && !isPropertyName) {
|
|
6015
|
+
const jsxExpr = node.parent
|
|
6016
|
+
const attr = jsxExpr.parent
|
|
6017
|
+
const isEventHandlerAttrValue =
|
|
6018
|
+
ts.isJsxExpression(jsxExpr) &&
|
|
6019
|
+
ts.isJsxAttribute(attr) &&
|
|
6020
|
+
attr.initializer === jsxExpr &&
|
|
6021
|
+
ts.isIdentifier(attr.name) &&
|
|
6022
|
+
isEventHandlerAttrName(attr.name.text)
|
|
6023
|
+
if (!isEventHandlerAttrValue) return true
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
return ts.forEachChild(node, findDisallowedRead)
|
|
6027
|
+
}
|
|
6028
|
+
return !bodyStatements.some(findDisallowedRead)
|
|
6029
|
+
}
|
|
6030
|
+
|
|
6031
|
+
/**
|
|
6032
|
+
* Whether a JSX attribute name is an event handler (`onClick`, `onInput`,
|
|
6033
|
+
* ...) rather than a plain attribute/prop. Shared by every site in this file
|
|
6034
|
+
* that needs the same call: native-element attribute extraction (routes into
|
|
6035
|
+
* `IREvent`, never rendered HTML), the preamble-verbatim-leaf wiring guard,
|
|
6036
|
+
* and {@link everyReadIsEventHandlerPropValue}'s elision safety check — one
|
|
6037
|
+
* decision, one implementation, per CLAUDE.md.
|
|
6038
|
+
*/
|
|
6039
|
+
function isEventHandlerAttrName(name: string): boolean {
|
|
6040
|
+
return /^on[A-Z]/.test(name)
|
|
5911
6041
|
}
|
|
5912
6042
|
|
|
5913
6043
|
/** Drop the trailing separator space from the final js segment. */
|
|
@@ -6261,7 +6391,7 @@ function processAttributes(
|
|
|
6261
6391
|
// they're wired up at hydration time (delegated event registration) and
|
|
6262
6392
|
// must not leak into rendered HTML. The DOM event name is lowercase
|
|
6263
6393
|
// (`click`, not `Click`), so strip the `on` prefix and downcase.
|
|
6264
|
-
if (
|
|
6394
|
+
if (isEventHandlerAttrName(rawName)) {
|
|
6265
6395
|
if (attr.initializer && ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
6266
6396
|
const eventName = rawName.slice(2).toLowerCase()
|
|
6267
6397
|
reportJsxBranchLocalInCallback(attr.initializer.expression, ctx)
|
|
@@ -7287,10 +7417,7 @@ function expressionWrapsJsx(node: ts.Expression): boolean {
|
|
|
7287
7417
|
* Refuse a component prop whose initializer is a ternary/array literally
|
|
7288
7418
|
* wrapping JSX (#2667) — see `expressionWrapsJsx`'s docstring and the call
|
|
7289
7419
|
* site above for why this can't fall through to the plain `expression`
|
|
7290
|
-
* AttrValue path (raw JSX syntax would splice into the emitted client
|
|
7291
|
-
* JS) and why the fragment-wrap escape is NOT offered here (BF021 message
|
|
7292
|
-
* below explains the unsoundness inline; see #2667's tracking issue for
|
|
7293
|
-
* the full door-inventory finding).
|
|
7420
|
+
* AttrValue path (raw JSX syntax would splice into the emitted client JS).
|
|
7294
7421
|
*/
|
|
7295
7422
|
function reportNakedJsxWrapperProp(
|
|
7296
7423
|
ctx: TransformContext,
|
|
@@ -7305,15 +7432,21 @@ function reportNakedJsxWrapperProp(
|
|
|
7305
7432
|
`Prop '${propName}' is ${shape} wrapping JSX (${jsxExpr.getText(ctx.sourceFile)}). ` +
|
|
7306
7433
|
`This shape is not compiled — only a JSX element/fragment given DIRECTLY as the prop value is.`,
|
|
7307
7434
|
suggestion: {
|
|
7308
|
-
message:
|
|
7309
|
-
`Move the conditional
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
`
|
|
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).`,
|
|
7317
7450
|
escape: [{ kind: 'rewrite' }],
|
|
7318
7451
|
},
|
|
7319
7452
|
}),
|
|
@@ -7368,14 +7501,13 @@ function processComponentProps(
|
|
|
7368
7501
|
// plain `expression` path below, which stringifies the initializer's
|
|
7369
7502
|
// SOURCE TEXT — literal JSX syntax spliced into the emitted client
|
|
7370
7503
|
// JS (invalid at runtime; the #2651 door inventory's discovery).
|
|
7371
|
-
// Refuse loudly instead of guessing a lowering
|
|
7372
|
-
// escape
|
|
7373
|
-
//
|
|
7374
|
-
//
|
|
7375
|
-
//
|
|
7376
|
-
//
|
|
7377
|
-
//
|
|
7378
|
-
// 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.
|
|
7379
7511
|
if (
|
|
7380
7512
|
(ts.isConditionalExpression(jsxExpr) || ts.isArrayLiteralExpression(jsxExpr)) &&
|
|
7381
7513
|
expressionWrapsJsx(jsxExpr)
|
|
@@ -7738,31 +7870,152 @@ function checkBareSignalOrMemoIdentifier(
|
|
|
7738
7870
|
* Structural AST check: is the array expression a direct prop reference?
|
|
7739
7871
|
*
|
|
7740
7872
|
* Returns true only when arrayExpr is:
|
|
7741
|
-
* (a) An Identifier that is a destructured prop binding (e.g. `toggleItems`)
|
|
7742
|
-
*
|
|
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)
|
|
7743
7881
|
*
|
|
7744
7882
|
* Unlike the regex-based `isPropsReference`, this avoids false positives from
|
|
7745
7883
|
* unrelated identifiers that happen to share a prop name (e.g. `state.items`
|
|
7746
|
-
* 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.
|
|
7747
7902
|
*/
|
|
7748
7903
|
function isArrayExprDirectPropRef(arrayExpr: ts.Expression, ctx: TransformContext): boolean {
|
|
7749
|
-
const propNames = new Set(ctx.patterns.props.map(p => p.name))
|
|
7750
7904
|
const propsObjName = ctx.analyzer.propsObjectName
|
|
7751
7905
|
|
|
7752
7906
|
if (ts.isIdentifier(arrayExpr)) {
|
|
7753
|
-
return
|
|
7907
|
+
return resolveAliasOrigin(propAliasHopCandidates(ctx), arrayExpr.text, name => (isDirectPropBindingName(name, ctx) ? true : null)) === true
|
|
7754
7908
|
}
|
|
7755
7909
|
|
|
7756
7910
|
if (ts.isPropertyAccessExpression(arrayExpr) && propsObjName) {
|
|
7757
7911
|
const obj = arrayExpr.expression
|
|
7758
|
-
if (ts.isIdentifier(obj)
|
|
7759
|
-
return true
|
|
7912
|
+
if (ts.isIdentifier(obj)) {
|
|
7913
|
+
return resolveAliasOrigin(propAliasHopCandidates(ctx), obj.text, name => (name === propsObjName ? true : null)) === true
|
|
7760
7914
|
}
|
|
7761
7915
|
}
|
|
7762
7916
|
|
|
7763
7917
|
return false
|
|
7764
7918
|
}
|
|
7765
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
|
+
|
|
7766
8019
|
/**
|
|
7767
8020
|
* Check if array expression is a signal or memo getter call.
|
|
7768
8021
|
* Used to determine if a loop needs mapArray for dynamic DOM updates.
|
|
@@ -7940,7 +8193,7 @@ function isPropsReference(expr: string, ctx: TransformContext, visited?: Set<str
|
|
|
7940
8193
|
function forwardsCallerRestProps(attrs: IRAttribute[], ctx: TransformContext): boolean {
|
|
7941
8194
|
for (const attr of attrs) {
|
|
7942
8195
|
if (attr.value.kind !== 'spread') continue
|
|
7943
|
-
const constants =
|
|
8196
|
+
const constants = constantValuesByName(ctx)
|
|
7944
8197
|
for (const name of new Set([attr.value.expr, attr.value.templateExpr])) {
|
|
7945
8198
|
if (!name) continue
|
|
7946
8199
|
if (resolveRestSpreadOriginCore(ctx.analyzer, constants, name) !== null) return true
|
|
@@ -7949,14 +8202,25 @@ function forwardsCallerRestProps(attrs: IRAttribute[], ctx: TransformContext): b
|
|
|
7949
8202
|
return false
|
|
7950
8203
|
}
|
|
7951
8204
|
|
|
7952
|
-
/** `ctx.analyzer.localConstants` indexed by name, memoized per component walk. */
|
|
7953
|
-
function
|
|
7954
|
-
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
|
|
7955
8208
|
const byName = new Map<string, string | undefined>()
|
|
7956
8209
|
for (const constant of ctx.analyzer.localConstants) {
|
|
7957
8210
|
if (!byName.has(constant.name)) byName.set(constant.name, constant.value)
|
|
7958
8211
|
}
|
|
7959
|
-
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
|
|
7960
8224
|
return byName
|
|
7961
8225
|
}
|
|
7962
8226
|
|