@barefootjs/jsx 0.31.3 → 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/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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +226 -163
- 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/relocate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/binding-scope-ratchet.test.ts +1 -1
- 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__/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 +2 -1
- package/src/adapters/template-imports.ts +93 -0
- package/src/debug.ts +4 -3
- package/src/identifier-pattern.ts +79 -0
- package/src/index.ts +1 -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 +18 -9
- package/src/module-exports.ts +3 -2
- package/src/relocate.ts +2 -1
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp, pickAttrMetaFromIR } from '../types.ts'
|
|
6
6
|
import type { ClientJsContext, ConditionalBranchChildComponent, ConditionalBranchReactiveAttr, BranchLoop, ConditionalBranchTextEffect, ConditionalElement, LoopChildBindings, LoopChildBranchSummary, LoopChildConditional, LoopOffset, NestedLoop } from './types.ts'
|
|
7
7
|
import { attrValueToString, freeIdsFromRefs, quotePropName, PROPS_PARAM } from './utils.ts'
|
|
8
|
-
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
8
|
+
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings, buildLoopRowScope } from './reactivity.ts'
|
|
9
9
|
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, renderFlatMapClientBody, renderFlatMapProjectionClientBody, flatMapCallbackHasKeyedLeaf, type SkeletonSlotPaths } from './html-template.ts'
|
|
10
10
|
import { templateRootIsSvg } from './control-flow/stringify/template-parse.ts'
|
|
11
11
|
import { expandDynamicPropValue, expandConstantForReactivity } from './prop-handling.ts'
|
|
12
12
|
import { walkIR, stopAt } from './walker.ts'
|
|
13
13
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
14
|
+
import { identifierPattern } from '../identifier-pattern.ts'
|
|
14
15
|
|
|
15
16
|
/** Expressions that render nothing (0 DOM nodes) — `&&` / `?:` empty branches. */
|
|
16
17
|
const EMPTY_RENDER_EXPRS = new Set(['null', 'undefined', 'false', "''", '""', '``'])
|
|
@@ -311,7 +312,7 @@ export function collectInnerLoops(
|
|
|
311
312
|
const template = n.children.map(c => irToPlaceholderTemplate(c, undefined, emitDepth, loopParamsForTemplate)).join('')
|
|
312
313
|
// Check if array expression references the outer loop param
|
|
313
314
|
const refsOuter = outerLoopParam
|
|
314
|
-
?
|
|
315
|
+
? identifierPattern(outerLoopParam).test(n.array)
|
|
315
316
|
: false
|
|
316
317
|
// Per-item bindings for inner loop body, collected uniformly when
|
|
317
318
|
// ctx is available: reactiveTexts / reactiveAttrs / refs are each
|
|
@@ -332,8 +333,8 @@ export function collectInnerLoops(
|
|
|
332
333
|
const innerPreambleNames = preambleNamesOf(n)
|
|
333
334
|
if (ctx) {
|
|
334
335
|
for (const child of n.children) {
|
|
335
|
-
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings))
|
|
336
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames))
|
|
336
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
|
|
337
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
|
|
337
338
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
338
339
|
}
|
|
339
340
|
}
|
|
@@ -375,6 +376,8 @@ export function collectInnerLoops(
|
|
|
375
376
|
siblingOffsets,
|
|
376
377
|
n.param,
|
|
377
378
|
n.paramBindings,
|
|
379
|
+
innerPreambleNames,
|
|
380
|
+
n.index,
|
|
378
381
|
))
|
|
379
382
|
}
|
|
380
383
|
}
|
|
@@ -671,7 +674,7 @@ export function collectElements(
|
|
|
671
674
|
const childHandlers: string[] = []
|
|
672
675
|
const bindings = projectionInner
|
|
673
676
|
? emptyLoopChildBindings()
|
|
674
|
-
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l))
|
|
677
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l), l.index)
|
|
675
678
|
if (!projectionInner) {
|
|
676
679
|
for (const child of l.children) {
|
|
677
680
|
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
@@ -1146,7 +1149,7 @@ function collectBranchLoops(
|
|
|
1146
1149
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1147
1150
|
// no-op for existing items.
|
|
1148
1151
|
const branchBindings = ctx && !projectionInner
|
|
1149
|
-
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n))
|
|
1152
|
+
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n), n.index)
|
|
1150
1153
|
: emptyLoopChildBindings()
|
|
1151
1154
|
|
|
1152
1155
|
loops.push({
|
|
@@ -1346,6 +1349,12 @@ export function collectLoopChildBindings(
|
|
|
1346
1349
|
* `collectLoopChildReactiveAttrs`. Omitted for a loop with no preamble.
|
|
1347
1350
|
*/
|
|
1348
1351
|
preambleNames?: ReadonlySet<string>,
|
|
1352
|
+
/**
|
|
1353
|
+
* The loop's index param name (`.map((item, i) => ...)`'s `i`), when
|
|
1354
|
+
* present — folded into the shadow guard (Copilot review on #2595) so
|
|
1355
|
+
* an index-shadowing const doesn't get const-folded either.
|
|
1356
|
+
*/
|
|
1357
|
+
loopIndex?: string | null,
|
|
1349
1358
|
): LoopChildBindings {
|
|
1350
1359
|
const bindings = emptyLoopChildBindings()
|
|
1351
1360
|
for (const child of children) {
|
|
@@ -1355,10 +1364,10 @@ export function collectLoopChildBindings(
|
|
|
1355
1364
|
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1356
1365
|
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1357
1366
|
// `.reactiveTexts`) — descending into them here too would double-bind.
|
|
1358
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames))
|
|
1359
|
-
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true))
|
|
1367
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex))
|
|
1368
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex))
|
|
1360
1369
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1361
|
-
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
1370
|
+
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex))
|
|
1362
1371
|
}
|
|
1363
1372
|
return bindings
|
|
1364
1373
|
}
|
|
@@ -1369,8 +1378,24 @@ export function collectLoopChildConditionals(
|
|
|
1369
1378
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1370
1379
|
loopParam?: string,
|
|
1371
1380
|
loopParamBindings?: readonly import('../types.ts').LoopParamBinding[],
|
|
1381
|
+
/**
|
|
1382
|
+
* The enclosing loop's `.map()` callback preamble locals (#2447), when
|
|
1383
|
+
* known — folded into the `expandConstantForReactivity` shadow guard
|
|
1384
|
+
* (#2482 Stage 1b) so a preamble local shadowing a component/module
|
|
1385
|
+
* const doesn't get const-folded into the condition, which would
|
|
1386
|
+
* corrupt `classifyReactivity`'s verdict below (a substituted literal
|
|
1387
|
+
* reads as "not reactive," silently freezing the branch at its initial
|
|
1388
|
+
* value instead of wiring an `insert()`).
|
|
1389
|
+
*/
|
|
1390
|
+
preambleNames?: ReadonlySet<string>,
|
|
1391
|
+
/**
|
|
1392
|
+
* The loop's index param name, when present — see
|
|
1393
|
+
* `collectLoopChildBindings`'s doc comment (Copilot review on #2595).
|
|
1394
|
+
*/
|
|
1395
|
+
loopIndex?: string | null,
|
|
1372
1396
|
): LoopChildConditional[] {
|
|
1373
1397
|
const conditionals: LoopChildConditional[] = []
|
|
1398
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
1374
1399
|
|
|
1375
1400
|
// Widen the source-level "references loop param" check so destructured
|
|
1376
1401
|
// callbacks fire too — the pattern text `[, cfg]` never word-matches on
|
|
@@ -1402,7 +1427,7 @@ export function collectLoopChildConditionals(
|
|
|
1402
1427
|
// Pre-gate using AST `reactive` flag on the source condition before
|
|
1403
1428
|
// paying for constant expansion — matches the legacy short-circuit.
|
|
1404
1429
|
if (!n.reactive && !refsLoopParamInSource) return
|
|
1405
|
-
const expanded = expandConstantForReactivity(n.condition, ctx, sourceFreeIds)
|
|
1430
|
+
const expanded = expandConstantForReactivity(n.condition, ctx, sourceFreeIds, scope)
|
|
1406
1431
|
// Loop-param conditionals are reactive via per-item signal accessors;
|
|
1407
1432
|
// classifyReactivity sees both paths (signal/memo/prop + loop-param).
|
|
1408
1433
|
if (classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
|
|
@@ -1421,8 +1446,8 @@ export function collectLoopChildConditionals(
|
|
|
1421
1446
|
condition: expanded.expr,
|
|
1422
1447
|
whenTrueHtml,
|
|
1423
1448
|
whenFalseHtml,
|
|
1424
|
-
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1425
|
-
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1449
|
+
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1450
|
+
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1426
1451
|
...(expanded.freeIds !== undefined && { conditionFreeIdentifiers: expanded.freeIds }),
|
|
1427
1452
|
})
|
|
1428
1453
|
},
|
|
@@ -1444,19 +1469,23 @@ function summarizeLoopChildBranch(
|
|
|
1444
1469
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1445
1470
|
loopParam?: string,
|
|
1446
1471
|
loopParamBindings?: readonly import('../types.ts').LoopParamBinding[],
|
|
1472
|
+
/** Enclosing loop's preamble locals (#2447) — see `collectLoopChildConditionals`. */
|
|
1473
|
+
preambleNames?: ReadonlySet<string>,
|
|
1474
|
+
/** Enclosing loop's index param name — see `collectLoopChildConditionals` (Copilot review on #2595). */
|
|
1475
|
+
loopIndex?: string | null,
|
|
1447
1476
|
): LoopChildBranchSummary {
|
|
1448
1477
|
const inner = collectInnerLoops([node], siblingOffsets, loopParam, ctx, branchInnerLoopOptions)
|
|
1449
1478
|
return {
|
|
1450
1479
|
childComponents: collectConditionalBranchChildComponents(node),
|
|
1451
1480
|
innerLoops: inner.length > 0 ? inner : undefined,
|
|
1452
|
-
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1481
|
+
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1453
1482
|
events: collectConditionalBranchEvents(node),
|
|
1454
1483
|
// Loop-param-aware — reuses the flat loop-item collectors scoped to just
|
|
1455
1484
|
// this branch's subtree. Both already stop descending into any further
|
|
1456
1485
|
// nested reactive conditional (own insert()/arm), so calling them here
|
|
1457
1486
|
// on the branch root yields exactly this branch's direct bindings
|
|
1458
1487
|
// without re-collecting what a nested arm already owns (#2347).
|
|
1459
|
-
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
|
|
1488
|
+
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex),
|
|
1460
1489
|
// Skip ONLY when the branch's entire content is a single bare
|
|
1461
1490
|
// `expression` (no wrapping element) that MAY yield a live DOM node —
|
|
1462
1491
|
// i.e. it contains a call anywhere (`node.hasFunctionCalls`, computed
|
|
@@ -1506,6 +1535,6 @@ function summarizeLoopChildBranch(
|
|
|
1506
1535
|
// two can't disagree on shape).
|
|
1507
1536
|
reactiveTexts: node.type === 'expression' && node.hasFunctionCalls
|
|
1508
1537
|
? []
|
|
1509
|
-
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true),
|
|
1538
|
+
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex),
|
|
1510
1539
|
}
|
|
1511
1540
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { TopLevelLoop, BranchLoop, LoopChildEvent } from '../../types.ts'
|
|
13
13
|
import { buildChainedArrayExpr, varSlotId, substituteLoopBindings } from '../../utils.ts'
|
|
14
|
+
import { identifierPattern } from '../../../identifier-pattern.ts'
|
|
14
15
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
15
16
|
import type {
|
|
16
17
|
EventDelegationPlan,
|
|
@@ -138,7 +139,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
138
139
|
// we substitute bindings with `item.<path>` directly (#951).
|
|
139
140
|
const keyWithItem = hasBindings
|
|
140
141
|
? substituteLoopBindings(args.key, args.paramBindings!, 'item')
|
|
141
|
-
: args.key.replace(
|
|
142
|
+
: args.key.replace(identifierPattern(args.param, 'g'), 'item')
|
|
142
143
|
return {
|
|
143
144
|
kind: 'keyed',
|
|
144
145
|
arrayExpr: args.array,
|
|
@@ -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
|
)
|