@barefootjs/jsx 0.31.2 → 0.31.4
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/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/template-imports.d.ts +27 -15
- package/dist/adapters/template-imports.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/identifier-pattern.d.ts +62 -0
- package/dist/identifier-pattern.d.ts.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +280 -119
- package/dist/ir-to-client-js/collect-elements.d.ts +23 -2
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +12 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +20 -12
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/prop-handling.d.ts +25 -2
- package/dist/ir-to-client-js/prop-handling.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +30 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/rewrite-props-object.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +1 -1
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/csr-materialize-loop-preamble-shadow.test.ts +76 -0
- package/src/__tests__/csr-substitute-enclosing-scope.test.ts +77 -0
- package/src/__tests__/identifier-pattern.test.ts +170 -0
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +107 -0
- package/src/__tests__/rewrite-dynamic-imports.test.ts +98 -0
- package/src/adapters/jsx-adapter.ts +34 -5
- package/src/adapters/template-imports.ts +93 -0
- package/src/analyzer.ts +20 -3
- package/src/debug.ts +4 -3
- package/src/identifier-pattern.ts +79 -0
- package/src/index.ts +5 -1
- package/src/ir-to-client-js/collect-elements.ts +44 -15
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +2 -1
- package/src/ir-to-client-js/csr-substitute.ts +19 -2
- package/src/ir-to-client-js/html-template.ts +37 -31
- package/src/ir-to-client-js/imports.ts +2 -1
- package/src/ir-to-client-js/prop-handling.ts +28 -1
- package/src/ir-to-client-js/reactivity.ts +54 -4
- package/src/ir-to-client-js/rewrite-props-object.ts +2 -1
- package/src/ir-to-client-js/utils.ts +9 -8
- package/src/jsx-to-ir.ts +187 -73
- package/src/module-exports.ts +3 -2
- package/src/prop-rewrite.ts +1 -1
- package/src/relocate.ts +2 -1
- package/src/scope/binding-scope.ts +238 -0
- package/src/types.ts +8 -0
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
import { toDomEventName, varSlotId, substituteLoopBindings, buildLoopChildIndexSubtraction, DATA_KEY, keyAttrName } from '../../utils.ts'
|
|
36
36
|
import { extractFreeIdentifiersFromText } from '../../csr-substitute.ts'
|
|
37
|
+
import { identifierPattern } from '../../../identifier-pattern.ts'
|
|
37
38
|
import type {
|
|
38
39
|
EventDelegationPlan,
|
|
39
40
|
KeyedItemLookup,
|
|
@@ -215,7 +216,7 @@ function emitKeyedLookup(
|
|
|
215
216
|
const rawKey = nested.key ?? ''
|
|
216
217
|
const innerKeyExpr = nested.paramBindings && nested.paramBindings.length > 0
|
|
217
218
|
? substituteLoopBindings(rawKey, nested.paramBindings, 'item')
|
|
218
|
-
: rawKey.replace(
|
|
219
|
+
: rawKey.replace(identifierPattern(nested.param, 'g'), 'item')
|
|
219
220
|
const outerRef = hasBindings ? '__bfLoopItem' : param
|
|
220
221
|
ls.push(` const ${nested.param} = ${outerRef} && ${nested.array}.find(item => String(${innerKeyExpr}) === innerKey${nested.depth})`)
|
|
221
222
|
}
|
|
@@ -28,6 +28,7 @@ import ts from 'typescript'
|
|
|
28
28
|
import { PROPS_PARAM, inferDefaultValue } from './utils.ts'
|
|
29
29
|
import { extractFreeIdentifiersFromNode } from '../analyzer.ts'
|
|
30
30
|
import type { MemoInfo, SignalInfo } from '../types.ts'
|
|
31
|
+
import type { BindingScope } from '../scope/binding-scope.ts'
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* CSR-substituted const value: the const's initializer with every
|
|
@@ -94,10 +95,21 @@ export interface CsrEnv {
|
|
|
94
95
|
* so chained inline expansions (const A inlines to a form that mentions
|
|
95
96
|
* const B's already-rewritten value) stay accurate without analytical
|
|
96
97
|
* bookkeeping.
|
|
98
|
+
*
|
|
99
|
+
* `enclosingScope` (#2482 Stage 1b): an optional `BindingScope` for loop
|
|
100
|
+
* bindings ENCLOSING this expression — names bound outside the
|
|
101
|
+
* expression's own AST, which the internal `boundStack` (built purely
|
|
102
|
+
* from arrow/function scopes found while walking `value` itself) can
|
|
103
|
+
* never see on its own. Every current call site already pre-filters
|
|
104
|
+
* `env.substitutions` for loop-shadowed names before calling in (see
|
|
105
|
+
* `html-template.ts`'s `loop` case), so this is defense-in-depth, not a
|
|
106
|
+
* currently-observable behavior change: the two mechanisms should agree
|
|
107
|
+
* on what's shadowed regardless of which one runs first.
|
|
97
108
|
*/
|
|
98
109
|
export function csrSubstitute(
|
|
99
110
|
value: string,
|
|
100
111
|
env: CsrEnv,
|
|
112
|
+
enclosingScope?: BindingScope,
|
|
101
113
|
): { rewritten: string; freeIdentifiers: ReadonlySet<string> } {
|
|
102
114
|
if (!value || value.trim().length === 0) {
|
|
103
115
|
return { rewritten: value, freeIdentifiers: new Set() }
|
|
@@ -111,7 +123,7 @@ export function csrSubstitute(
|
|
|
111
123
|
let current = value
|
|
112
124
|
let lastFreeIdentifiers: ReadonlySet<string> = new Set()
|
|
113
125
|
for (let i = 0; i < maxIter; i++) {
|
|
114
|
-
const step = csrSubstituteOnce(current, env)
|
|
126
|
+
const step = csrSubstituteOnce(current, env, enclosingScope)
|
|
115
127
|
lastFreeIdentifiers = step.freeIdentifiers
|
|
116
128
|
if (step.rewritten === current) break
|
|
117
129
|
current = step.rewritten
|
|
@@ -122,6 +134,7 @@ export function csrSubstitute(
|
|
|
122
134
|
function csrSubstituteOnce(
|
|
123
135
|
value: string,
|
|
124
136
|
env: CsrEnv,
|
|
137
|
+
enclosingScope?: BindingScope,
|
|
125
138
|
): { rewritten: string; freeIdentifiers: ReadonlySet<string> } {
|
|
126
139
|
if (!value || value.trim().length === 0) {
|
|
127
140
|
return { rewritten: value, freeIdentifiers: new Set() }
|
|
@@ -151,12 +164,16 @@ function csrSubstituteOnce(
|
|
|
151
164
|
|
|
152
165
|
// Track the bound names in nested arrow/function scopes so we don't
|
|
153
166
|
// substitute identifiers shadowed by a parameter or local binding.
|
|
167
|
+
// `enclosingScope` seeds the OUTER frames — loop bindings introduced
|
|
168
|
+
// outside `value`'s own AST (item / index / destructure / preamble) —
|
|
169
|
+
// so a name bound there is treated identically to one bound by an
|
|
170
|
+
// arrow/function found while walking `value` itself.
|
|
154
171
|
const boundStack: Array<Set<string>> = []
|
|
155
172
|
const isBound = (name: string): boolean => {
|
|
156
173
|
for (let i = boundStack.length - 1; i >= 0; i--) {
|
|
157
174
|
if (boundStack[i].has(name)) return true
|
|
158
175
|
}
|
|
159
|
-
return false
|
|
176
|
+
return enclosingScope?.isBound(name) ?? false
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
const recordSubstitution = (start: number, end: number, sub: CsrSubstitution): void => {
|
|
@@ -13,6 +13,7 @@ import type { ClientJsContext } from './types.ts'
|
|
|
13
13
|
import { BF_PARENT_SCOPE_PLACEHOLDER, BF_SCOPE, escapeHtml } from '@barefootjs/shared'
|
|
14
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
15
|
import { derivesScopeFromSlot } from '../adapters/child-scope.ts'
|
|
16
|
+
import { BindingScope } from '../scope/binding-scope.ts'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Protect string literals from regex-based replacements.
|
|
@@ -1572,19 +1573,26 @@ export interface TemplateOptions {
|
|
|
1572
1573
|
csrEnv?: CsrEnv
|
|
1573
1574
|
loopDepth?: number
|
|
1574
1575
|
/**
|
|
1575
|
-
*
|
|
1576
|
-
* destructured
|
|
1577
|
-
* recursion
|
|
1578
|
-
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1576
|
+
* `BindingScope` for the ENCLOSING loops' callback bindings (item /
|
|
1577
|
+
* index / destructured names / preamble locals), threaded through the
|
|
1578
|
+
* recursion one `enterLoopRow` per nesting level (#2482 Stage 1b —
|
|
1579
|
+
* migrated from the ad-hoc flat loop-bound-names `ReadonlySet<string>`
|
|
1580
|
+
* this field replaces). Inside a loop body, `csrSubstitute` receives each
|
|
1581
|
+
* expression in isolation — no enclosing arrow text — so its own
|
|
1582
|
+
* bound-name tracking can't see the loop binding; the `loop` case
|
|
1583
|
+
* instead filters these names out of the child recursion's `csrEnv`
|
|
1584
|
+
* (via `scope.boundNames()`, the SHADOW-GUARD query — see
|
|
1585
|
+
* `BindingScope.valueBoundNames`'s doc comment for why this must stay
|
|
1586
|
+
* the all-sources query, not the value-only one), so an inlinable
|
|
1587
|
+
* const / signal / memo whose name is shadowed by a loop-row binding —
|
|
1588
|
+
* including a `.map()` callback preamble local (#2447) — never
|
|
1589
|
+
* substitutes at the shadowed occurrence. Scope-accurate per nesting
|
|
1590
|
+
* level (the CURRENT level's own `transformExpr` — e.g. the loop's
|
|
1591
|
+
* array-source expression — keeps the unfiltered env). `undefined` at
|
|
1592
|
+
* the top of the recursion means "no enclosing loop" (treated as
|
|
1593
|
+
* `BindingScope.EMPTY`).
|
|
1586
1594
|
*/
|
|
1587
|
-
|
|
1595
|
+
scope?: BindingScope
|
|
1588
1596
|
/** Emit `bf-s` placeholder on scoped elements inside a jsx-children prop (#1320). */
|
|
1589
1597
|
inHoistedChildren?: boolean
|
|
1590
1598
|
/**
|
|
@@ -2226,7 +2234,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2226
2234
|
// the IR — emit does no string transformation of its own (#1277).
|
|
2227
2235
|
const source = templateExpr ?? expr
|
|
2228
2236
|
if (!source) return source
|
|
2229
|
-
const { rewritten, freeIdentifiers } = csrSubstitute(source, env)
|
|
2237
|
+
const { rewritten, freeIdentifiers } = csrSubstitute(source, env, opts.scope)
|
|
2230
2238
|
|
|
2231
2239
|
// The CSR template runs at module scope, so any post-substitution
|
|
2232
2240
|
// free identifier that lands in `unsafeLocalNames` (init-body-only
|
|
@@ -2458,23 +2466,21 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2458
2466
|
}
|
|
2459
2467
|
|
|
2460
2468
|
case 'loop': {
|
|
2461
|
-
//
|
|
2462
|
-
//
|
|
2463
|
-
// const / signal / memo whose name the callback shadows
|
|
2464
|
-
//
|
|
2465
|
-
//
|
|
2466
|
-
//
|
|
2467
|
-
//
|
|
2468
|
-
//
|
|
2469
|
-
//
|
|
2470
|
-
//
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
}
|
|
2477
|
-
if (node.index) boundHere.add(node.index)
|
|
2469
|
+
// Enter this loop's row frame and filter its bound names out of the
|
|
2470
|
+
// child recursion's substitution env (#2222, #2482 Stage 1b): an
|
|
2471
|
+
// inlinable const / signal / memo whose name the callback shadows —
|
|
2472
|
+
// item, index, a destructured binding, OR a `.map()` callback
|
|
2473
|
+
// preamble local (#2447) — must never substitute inside the body.
|
|
2474
|
+
// `enterLoopRow` mirrors `jsx-to-ir.ts`'s legacy mutable loop-param
|
|
2475
|
+
// set semantics exactly (see its doc comment): a raw pattern-text `param` (the
|
|
2476
|
+
// BF025 fallback shapes) with no `paramBindings` still gets added,
|
|
2477
|
+
// but as non-identifier pattern text it can never collide with a
|
|
2478
|
+
// real const/signal/memo name, so it's a harmless no-op for this
|
|
2479
|
+
// filter. `boundNames()` (not `valueBoundNames()`) is the correct
|
|
2480
|
+
// query here — this is a SHADOW GUARD (every binding source must
|
|
2481
|
+
// hide an outer same-named const), not a reactivity classifier.
|
|
2482
|
+
const childScope = (opts.scope ?? BindingScope.EMPTY).enterLoopRow(node)
|
|
2483
|
+
const boundHere = childScope.boundNames()
|
|
2478
2484
|
const childEnv: CsrEnv = {
|
|
2479
2485
|
...env,
|
|
2480
2486
|
substitutions: new Map([...env.substitutions].filter(([name]) => !boundHere.has(name))),
|
|
@@ -2483,7 +2489,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2483
2489
|
...opts,
|
|
2484
2490
|
loopDepth: loopDepth + 1,
|
|
2485
2491
|
inHoistedChildren: false,
|
|
2486
|
-
|
|
2492
|
+
scope: childScope,
|
|
2487
2493
|
csrEnv: childEnv,
|
|
2488
2494
|
})
|
|
2489
2495
|
let childTemplate = node.children.map(recurseInLoopBody).join('')
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { ComponentIR, IRNode } from '../types.ts'
|
|
6
6
|
import { isClientBuiltinName } from '../builtins.ts'
|
|
7
7
|
import { collectValueReferencedNames } from '../value-references.ts'
|
|
8
|
+
import { identifierCallPattern } from '../identifier-pattern.ts'
|
|
8
9
|
|
|
9
10
|
// All exports from @barefootjs/client/runtime that may be used in generated code
|
|
10
11
|
export const RUNTIME_IMPORT_CANDIDATES = [
|
|
@@ -57,7 +58,7 @@ export function detectUsedImports(code: string): Set<string> {
|
|
|
57
58
|
const used = new Set<string>()
|
|
58
59
|
for (const name of RUNTIME_IMPORT_CANDIDATES) {
|
|
59
60
|
// Match function calls: name(
|
|
60
|
-
if (
|
|
61
|
+
if (identifierCallPattern(name).test(code)) {
|
|
61
62
|
used.add(name)
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ParamInfo, SignalInfo } from '../types.ts'
|
|
6
6
|
import type { ClientJsContext } from './types.ts'
|
|
7
|
+
import type { BindingScope } from '../scope/binding-scope.ts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Expand dynamic prop value by resolving local constants.
|
|
@@ -11,9 +12,22 @@ import type { ClientJsContext } from './types.ts'
|
|
|
11
12
|
* Per spec/compiler.md, no prop reference transformation is needed:
|
|
12
13
|
* - Destructured props are captured once at hydration, used as-is
|
|
13
14
|
* - Props object already uses props.xxx syntax
|
|
15
|
+
*
|
|
16
|
+
* `scope` (#2482 Stage 1b): the enclosing loop row's `BindingScope`, when
|
|
17
|
+
* `value` is being expanded from inside a `.map()` row. `ClientJsContext`
|
|
18
|
+
* itself carries no loop-scope field — it's a per-component object, built
|
|
19
|
+
* once and shared across the whole tree — so without this guard a loop
|
|
20
|
+
* row binding (item / index / destructured / preamble local) that shares
|
|
21
|
+
* a name with a component/module-level const resolves to the OUTER
|
|
22
|
+
* const's value instead of staying an unresolved reference to the row's
|
|
23
|
+
* own binding. `isBound` (not `valueBoundNames`) is the correct query —
|
|
24
|
+
* this is a SHADOW GUARD (every binding source hides an outer same-named
|
|
25
|
+
* const), not a reactivity classifier — see `BindingScope.valueBoundNames`'s
|
|
26
|
+
* doc comment.
|
|
14
27
|
*/
|
|
15
|
-
export function expandDynamicPropValue(value: string, ctx: ClientJsContext): string {
|
|
28
|
+
export function expandDynamicPropValue(value: string, ctx: ClientJsContext, scope?: BindingScope): string {
|
|
16
29
|
const trimmedValue = value.trim()
|
|
30
|
+
if (scope?.isBound(trimmedValue)) return value
|
|
17
31
|
|
|
18
32
|
const constant = ctx.localConstants.find((c) => c.name === trimmedValue)
|
|
19
33
|
if (constant && constant.value) {
|
|
@@ -35,16 +49,29 @@ export function expandDynamicPropValue(value: string, ctx: ClientJsContext): str
|
|
|
35
49
|
* Otherwise it is `originalFreeIds` passed by the caller (typically derived
|
|
36
50
|
* from the IR node's `origin.freeRefs`) — `undefined` when the caller has
|
|
37
51
|
* no precomputed set.
|
|
52
|
+
*
|
|
53
|
+
* `scope` (#2482 Stage 1b): see `expandDynamicPropValue` — same guard, same
|
|
54
|
+
* reasoning. Without it, a loop row's own binding (bare identifier, e.g. a
|
|
55
|
+
* `.map()` callback preamble local or the item param itself) that shares a
|
|
56
|
+
* name with a component/module-level const gets const-folded into the
|
|
57
|
+
* OUTER value here, which then corrupts BOTH the emitted expression AND
|
|
58
|
+
* downstream reactivity classification (`classifyReactivity` sees a
|
|
59
|
+
* literal instead of a live reference and concludes "not reactive" — the
|
|
60
|
+
* observable failure mode is a reactive attribute/conditional that
|
|
61
|
+
* silently freezes at its initial value instead of updating).
|
|
38
62
|
*/
|
|
39
63
|
export function expandConstantForReactivity(
|
|
40
64
|
expr: string,
|
|
41
65
|
ctx: ClientJsContext,
|
|
42
66
|
originalFreeIds?: ReadonlySet<string>,
|
|
67
|
+
scope?: BindingScope,
|
|
43
68
|
): { expr: string; freeIds: ReadonlySet<string> | undefined } {
|
|
44
69
|
// Stateful components use props.xxx directly — reactivity is already detected.
|
|
45
70
|
if (ctx.propsObjectName) return { expr, freeIds: originalFreeIds }
|
|
46
71
|
|
|
47
72
|
const trimmedValue = expr.trim()
|
|
73
|
+
if (scope?.isBound(trimmedValue)) return { expr, freeIds: originalFreeIds }
|
|
74
|
+
|
|
48
75
|
const constant = ctx.localConstants.find((c) => c.name === trimmedValue)
|
|
49
76
|
if (constant && constant.value) {
|
|
50
77
|
return { expr: constant.value, freeIds: constant.freeIdentifiers }
|
|
@@ -18,6 +18,41 @@ import { attrValueToString, freeIdsFromRefs, tokenContainsIdent } from './utils.
|
|
|
18
18
|
import { expandConstantForReactivity } from './prop-handling.ts'
|
|
19
19
|
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
20
20
|
import { walkIR, stopAt } from './walker.ts'
|
|
21
|
+
import { BindingScope } from '../scope/binding-scope.ts'
|
|
22
|
+
import { identifierCallPattern } from '../identifier-pattern.ts'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the `BindingScope` for one loop row's own bindings — item /
|
|
26
|
+
* destructured param / index names plus (when supplied) the `.map()`
|
|
27
|
+
* callback's preamble locals (#2447) — for the `expandConstantForReactivity`
|
|
28
|
+
* shadow guard (#2482 Stage 1b). `undefined` when there's no enclosing loop
|
|
29
|
+
* (`loopParam` unset), matching every caller's existing "no loop" shape.
|
|
30
|
+
*
|
|
31
|
+
* `loopIndex` (Copilot review on #2595): a `.map((item, i) => ...)`
|
|
32
|
+
* callback's second param is a real row-scoped binding exactly like
|
|
33
|
+
* `item` — an `i`-shadows-a-module-const attr/text (e.g.
|
|
34
|
+
* `data-idx={/* @client *\/ i}`) const-folds to the outer value exactly
|
|
35
|
+
* like an item-param shadow does when the index name isn't in scope.
|
|
36
|
+
* Only guarded when the CALLER passes it in, though — none of the
|
|
37
|
+
* `collectLoopChild*` collectors' own callers threaded the IR loop's
|
|
38
|
+
* `index` field through before this fix (see each call site's own
|
|
39
|
+
* `undefined`/omitted-arg default), so `undefined` here still means "not
|
|
40
|
+
* guarded," not "no index param."
|
|
41
|
+
*/
|
|
42
|
+
export function buildLoopRowScope(
|
|
43
|
+
loopParam?: string,
|
|
44
|
+
loopParamBindings?: readonly LoopParamBinding[],
|
|
45
|
+
preambleNames?: ReadonlySet<string>,
|
|
46
|
+
loopIndex?: string | null,
|
|
47
|
+
): BindingScope | undefined {
|
|
48
|
+
if (!loopParam) return undefined
|
|
49
|
+
return BindingScope.EMPTY.enterLoopRow({
|
|
50
|
+
param: loopParam,
|
|
51
|
+
paramBindings: loopParamBindings,
|
|
52
|
+
index: loopIndex,
|
|
53
|
+
preamble: preambleNames && preambleNames.size > 0 ? { declaredNames: [...preambleNames] } : undefined,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
21
56
|
|
|
22
57
|
/**
|
|
23
58
|
* Phase 2 reactivity detection: determines if a code expression needs `createEffect`
|
|
@@ -146,13 +181,13 @@ export function needsEffectWrapper(
|
|
|
146
181
|
freeIdentifiers?: ReadonlySet<string>,
|
|
147
182
|
): boolean {
|
|
148
183
|
for (const signal of ctx.signals) {
|
|
149
|
-
if (
|
|
184
|
+
if (identifierCallPattern(signal.getter).test(expr)) {
|
|
150
185
|
return true
|
|
151
186
|
}
|
|
152
187
|
}
|
|
153
188
|
|
|
154
189
|
for (const memo of ctx.memos) {
|
|
155
|
-
if (
|
|
190
|
+
if (identifierCallPattern(memo.name).test(expr)) {
|
|
156
191
|
return true
|
|
157
192
|
}
|
|
158
193
|
}
|
|
@@ -555,6 +590,14 @@ function traverseForComponents(
|
|
|
555
590
|
* Defaults to false: some callers (e.g. a plain nested `.map()`'s own
|
|
556
591
|
* per-item bindings) have no sibling conditional collector, so a slotId'd
|
|
557
592
|
* conditional's inlined content is only ever reachable by descending here.
|
|
593
|
+
*
|
|
594
|
+
* `preambleNames` (#2482 Stage 1b): the enclosing loop's `.map()` callback
|
|
595
|
+
* preamble locals (#2447), when known — folded into the `expandConstantForReactivity`
|
|
596
|
+
* shadow guard via `buildLoopRowScope` so a preamble local shadowing a
|
|
597
|
+
* component/module const doesn't get const-folded here.
|
|
598
|
+
*
|
|
599
|
+
* `loopIndex` (Copilot review on #2595): the loop's index param name
|
|
600
|
+
* (`.map((item, i) => ...)`'s `i`), when known — see `buildLoopRowScope`.
|
|
558
601
|
*/
|
|
559
602
|
export function collectLoopChildReactiveTexts(
|
|
560
603
|
node: IRNode,
|
|
@@ -562,8 +605,11 @@ export function collectLoopChildReactiveTexts(
|
|
|
562
605
|
loopParam?: string,
|
|
563
606
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
564
607
|
stopAtReactiveConditionals = false,
|
|
608
|
+
preambleNames?: ReadonlySet<string>,
|
|
609
|
+
loopIndex?: string | null,
|
|
565
610
|
): LoopChildReactiveText[] {
|
|
566
611
|
const texts: LoopChildReactiveText[] = []
|
|
612
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
567
613
|
walkIR(node, false, {
|
|
568
614
|
// Skip loop/async/if-statement subtrees — the original walker omitted
|
|
569
615
|
// them; they have their own scopes (inner-loop reconciliation, async
|
|
@@ -579,7 +625,7 @@ export function collectLoopChildReactiveTexts(
|
|
|
579
625
|
// (a `joinArrayChild` region's value is raw HTML, not text).
|
|
580
626
|
if (n.preambleRegion) return
|
|
581
627
|
const originFreeIds = freeIdsFromRefs(n.origin?.freeRefs)
|
|
582
|
-
const expanded = expandConstantForReactivity(n.expr, ctx, originFreeIds)
|
|
628
|
+
const expanded = expandConstantForReactivity(n.expr, ctx, originFreeIds, scope)
|
|
583
629
|
// Include if expression reads signals OR references the loop parameter
|
|
584
630
|
// (loop param becomes a signal accessor via per-item signals). Falls
|
|
585
631
|
// back to the Solid-style AST-flag wrap decision — mirroring
|
|
@@ -621,6 +667,8 @@ export function collectLoopChildReactiveTexts(
|
|
|
621
667
|
* `stopAtReactiveConditionals` (#2347): see `collectLoopChildReactiveTexts` —
|
|
622
668
|
* same semantics. Pass true only when the caller also separately collects
|
|
623
669
|
* nested reactive conditionals and gives each its own `insert()`.
|
|
670
|
+
*
|
|
671
|
+
* `loopIndex` (Copilot review on #2595): see `collectLoopChildReactiveTexts`.
|
|
624
672
|
*/
|
|
625
673
|
/** Does any name in `names` appear in `set`? Iterates rather than
|
|
626
674
|
* spreading — this runs per attribute (Copilot review). */
|
|
@@ -636,8 +684,10 @@ export function collectLoopChildReactiveAttrs(
|
|
|
636
684
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
637
685
|
stopAtReactiveConditionals = false,
|
|
638
686
|
preambleNames?: ReadonlySet<string>,
|
|
687
|
+
loopIndex?: string | null,
|
|
639
688
|
): LoopChildReactiveAttr[] {
|
|
640
689
|
const attrs: LoopChildReactiveAttr[] = []
|
|
690
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
641
691
|
traverseElements(node, (el) => {
|
|
642
692
|
if (el.slotId) {
|
|
643
693
|
for (const attr of el.attrs) {
|
|
@@ -653,7 +703,7 @@ export function collectLoopChildReactiveAttrs(
|
|
|
653
703
|
if (attr.name === 'key') continue
|
|
654
704
|
const valueStr = attrValueToString(attr.value)
|
|
655
705
|
if (!valueStr) continue
|
|
656
|
-
const expanded = expandConstantForReactivity(valueStr, ctx, attr.freeIdentifiers)
|
|
706
|
+
const expanded = expandConstantForReactivity(valueStr, ctx, attr.freeIdentifiers, scope)
|
|
657
707
|
// `/* @client */` always defers via per-item createEffect
|
|
658
708
|
// regardless of the reactivity classifier — matches the
|
|
659
709
|
// top-level `collectElements.element` and the conditional
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import ts from 'typescript'
|
|
26
26
|
import { PROPS_PARAM } from './utils.ts'
|
|
27
|
+
import { identifierPattern } from '../identifier-pattern.ts'
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Rename every value-position reference to `propsObjectName` in `code`
|
|
@@ -36,7 +37,7 @@ export function rewritePropsObjectRef(code: string, propsObjectName: string | nu
|
|
|
36
37
|
if (srcPropsName === PROPS_PARAM) return code
|
|
37
38
|
|
|
38
39
|
// Quick exit when the name doesn't appear at all.
|
|
39
|
-
if (!
|
|
40
|
+
if (!identifierPattern(srcPropsName).test(code)) return code
|
|
40
41
|
|
|
41
42
|
const sourceFile = ts.createSourceFile(
|
|
42
43
|
'init-body.ts',
|
|
@@ -8,6 +8,7 @@ import type { AttrValue, IRTemplatePart, LoopParamBinding, FreeReference, IRNode
|
|
|
8
8
|
import type { TopLevelLoop, BranchLoop, LoopOffset } from './types.ts'
|
|
9
9
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
10
10
|
import { templatePartsToJsExpr } from '../template-parts.ts'
|
|
11
|
+
import { escapeIdentifierForRegex, ID_BOUNDARY_BEFORE, ID_BOUNDARY_AFTER } from '../identifier-pattern.ts'
|
|
11
12
|
import {
|
|
12
13
|
iterateJsTokens,
|
|
13
14
|
isIdentifierLikeToken,
|
|
@@ -337,10 +338,6 @@ export function inferDefaultValue(type: { kind: string; primitive?: string }): s
|
|
|
337
338
|
return 'undefined'
|
|
338
339
|
}
|
|
339
340
|
|
|
340
|
-
function escapeRegExp(s: string): string {
|
|
341
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
342
|
-
}
|
|
343
|
-
|
|
344
341
|
/**
|
|
345
342
|
* Flatten an `OriginInfo.freeRefs` list to a plain `Set<string>` of free
|
|
346
343
|
* identifier names (#1267). Skips `kind: 'reactive-brand'` entries whose
|
|
@@ -561,8 +558,12 @@ export function wrapLoopParamAsAccessor(expr: string, paramName: string, binding
|
|
|
561
558
|
if (bindings && bindings.length > 0) {
|
|
562
559
|
return rewriteLoopBindingRefs(expr, bindings, '__bfItem()')
|
|
563
560
|
}
|
|
564
|
-
|
|
565
|
-
|
|
561
|
+
// `paramName` is a legal JS identifier and may itself start with `$`
|
|
562
|
+
// (`$1`, `$&`, …) — a plain string replacement would risk `String.replace`
|
|
563
|
+
// reading those as backreference/whole-match sequences, so use a replacer
|
|
564
|
+
// function to insert the accessor text literally (#2592).
|
|
565
|
+
const re = new RegExp(`${ID_BOUNDARY_BEFORE}${escapeIdentifierForRegex(paramName)}(?!\\s*\\()(?!-)${ID_BOUNDARY_AFTER}`, 'gu')
|
|
566
|
+
return replaceInExprContexts(expr, re, () => `${paramName}()`)
|
|
566
567
|
}
|
|
567
568
|
|
|
568
569
|
/**
|
|
@@ -588,8 +589,8 @@ function rewriteLoopBindingRefs(
|
|
|
588
589
|
const byName = new Map<string, LoopParamBinding>()
|
|
589
590
|
for (const b of bindings) byName.set(b.name, b)
|
|
590
591
|
const preprocessed = expandShorthandBindings(expr, new Set(byName.keys()))
|
|
591
|
-
const alt = bindings.map(b =>
|
|
592
|
-
const re = new RegExp(
|
|
592
|
+
const alt = bindings.map(b => escapeIdentifierForRegex(b.name)).join('|')
|
|
593
|
+
const re = new RegExp(`${ID_BOUNDARY_BEFORE}(${alt})${ID_BOUNDARY_AFTER}`, 'gu')
|
|
593
594
|
return replaceInExprContexts(preprocessed, re, (_m: string, name: string) =>
|
|
594
595
|
renderLoopBindingAccess(byName.get(name)!, accessor),
|
|
595
596
|
)
|