@barefootjs/jsx 0.31.3 → 0.31.5
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/loop-bound-names.d.ts +18 -0
- package/dist/adapters/loop-bound-names.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/adapters/test-adapter.d.ts.map +1 -1
- package/dist/augment-inherited-props.d.ts +12 -2
- package/dist/augment-inherited-props.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/debug.d.ts.map +1 -1
- package/dist/free-refs.d.ts +11 -2
- package/dist/free-refs.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 +968 -789
- 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/plan/build-reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.d.ts +7 -0
- package/dist/ir-to-client-js/control-flow/plan/reactive-effects.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 +46 -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/types.d.ts +16 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +15 -0
- 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 +64 -0
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/binding-scope-ratchet.test.ts +146 -21
- package/src/__tests__/component-type-parameters.test.ts +70 -0
- package/src/__tests__/csr-materialize-loop-preamble-shadow.test.ts +85 -0
- package/src/__tests__/csr-substitute-enclosing-scope.test.ts +77 -0
- package/src/__tests__/free-refs.test.ts +1 -1
- 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__/mutable-binding-writers.test.ts +133 -0
- package/src/__tests__/preamble-conditional-reactivity.test.ts +191 -0
- package/src/__tests__/rewrite-dynamic-imports.test.ts +98 -0
- package/src/__tests__/signal-setter-updater-type.test.ts +81 -0
- package/src/adapters/jsx-adapter.ts +31 -4
- package/src/adapters/loop-bound-names.ts +18 -0
- package/src/adapters/template-imports.ts +93 -0
- package/src/adapters/test-adapter.ts +4 -1
- package/src/augment-inherited-props.ts +13 -1
- package/src/compiler.ts +18 -0
- package/src/debug.ts +38 -24
- package/src/free-refs.ts +14 -5
- package/src/identifier-pattern.ts +79 -0
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/collect-elements.ts +60 -16
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +2 -1
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +12 -2
- package/src/ir-to-client-js/csr-substitute.ts +19 -2
- package/src/ir-to-client-js/html-template.ts +42 -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 +58 -6
- package/src/ir-to-client-js/rewrite-props-object.ts +2 -1
- package/src/ir-to-client-js/types.ts +16 -0
- package/src/ir-to-client-js/utils.ts +24 -8
- package/src/jsx-to-ir.ts +135 -10
- package/src/module-exports.ts +140 -2
- package/src/relocate.ts +2 -1
- package/src/scope/binding-scope.ts +1 -1
- package/src/types.ts +41 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single door for building "does this expression text reference identifier
|
|
3
|
+
* X?" regexes (#2592).
|
|
4
|
+
*
|
|
5
|
+
* The naive `new RegExp(`\\b${name}\\b`)` idiom used throughout the compiler
|
|
6
|
+
* breaks for `$`-containing identifiers (legal in JS: `$item`, `item$`,
|
|
7
|
+
* `a$b`) in two independent ways:
|
|
8
|
+
*
|
|
9
|
+
* 1. Unescaped: an interpolated `$` is a regex metacharacter (end-of-
|
|
10
|
+
* input/line anchor), so `\b$item\b` / `\ba$b\b` can (almost) never
|
|
11
|
+
* match mid-string — false negative.
|
|
12
|
+
* 2. Even escaped, `\b` requires a `\w`/non-`\w` transition and `$` is
|
|
13
|
+
* not `\w` (`[A-Za-z0-9_]`) — so `\b\$item\b` still fails to match the
|
|
14
|
+
* leading boundary in `($item)` (both `(` and `$` are non-word, so no
|
|
15
|
+
* transition occurs there).
|
|
16
|
+
*
|
|
17
|
+
* `identifierPattern` / `identifierCallPattern` fix both: the identifier
|
|
18
|
+
* text is escaped before interpolation, and the boundary is asserted with
|
|
19
|
+
* lookaround against `\p{ID_Continue}` (Unicode "can continue an
|
|
20
|
+
* identifier") unioned with `$`, so `$` is correctly treated as
|
|
21
|
+
* identifier-like on both sides of the match.
|
|
22
|
+
*
|
|
23
|
+
* Scope: these remain the same *bounded lexical heuristic* the compiler has
|
|
24
|
+
* always used for expression-text scanning (not a general JS/TS parse —
|
|
25
|
+
* see CLAUDE.md's structural-parsing rule, which does not apply to this
|
|
26
|
+
* class of check). This module only fixes the `$` boundary bug; it does not
|
|
27
|
+
* change what the heuristic considers a "reference" (string literals,
|
|
28
|
+
* comments, and member-access tails are still opaque to it — callers that
|
|
29
|
+
* need that precision use `tokenContainsIdent` / `node.freeIdentifiers`
|
|
30
|
+
* instead, per #1267).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Duplicate regex flags throw at construction ('gu' + 'u' -> SyntaxError),
|
|
34
|
+
// so `u` is added only when the caller didn't already pass it.
|
|
35
|
+
function withUnicodeFlag(flags: string): string {
|
|
36
|
+
return flags.includes('u') ? flags : `${flags}u`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Escape regex metacharacters in a literal identifier before interpolation. */
|
|
40
|
+
export function escapeIdentifierForRegex(name: string): string {
|
|
41
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lookaround fragments asserting "not preceded/followed by an identifier
|
|
46
|
+
* continuation character (including `$`)". `$` is a legal identifier char
|
|
47
|
+
* in JS but is not `\p{ID_Continue}`, so it's unioned in explicitly.
|
|
48
|
+
*
|
|
49
|
+
* Exported for the few call sites that must splice extra assertions
|
|
50
|
+
* between the identifier and the trailing boundary (e.g. "not followed by
|
|
51
|
+
* a call" as well as "not followed by an identifier char") — compose with
|
|
52
|
+
* these fragments rather than reintroducing a bare `\b`.
|
|
53
|
+
*/
|
|
54
|
+
export const ID_BOUNDARY_BEFORE = '(?<![\\p{ID_Continue}$])'
|
|
55
|
+
export const ID_BOUNDARY_AFTER = '(?![\\p{ID_Continue}$])'
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Regex matching a standalone reference to identifier `name` — the `$`-safe
|
|
59
|
+
* replacement for `new RegExp(`\\b${name}\\b`)`. Always carries the `u`
|
|
60
|
+
* (unicode) flag, required for `\p{ID_Continue}`; pass additional flags
|
|
61
|
+
* (e.g. `'g'` for `String.replace`/`matchAll` substitution sites) via
|
|
62
|
+
* `flags`.
|
|
63
|
+
*/
|
|
64
|
+
export function identifierPattern(name: string, flags = ''): RegExp {
|
|
65
|
+
const esc = escapeIdentifierForRegex(name)
|
|
66
|
+
return new RegExp(`${ID_BOUNDARY_BEFORE}${esc}${ID_BOUNDARY_AFTER}`, withUnicodeFlag(flags))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Regex matching identifier `name` used in call position (`name(...)`,
|
|
71
|
+
* allowing whitespace before the paren) — the `$`-safe replacement for
|
|
72
|
+
* `new RegExp(`\\b${name}\\s*\\(`)`. No trailing boundary assertion is
|
|
73
|
+
* needed: `\s`/`(` are already not `\p{ID_Continue}`/`$`, so they can't be
|
|
74
|
+
* mistaken for a continuation of `name`.
|
|
75
|
+
*/
|
|
76
|
+
export function identifierCallPattern(name: string, flags = ''): RegExp {
|
|
77
|
+
const esc = escapeIdentifierForRegex(name)
|
|
78
|
+
return new RegExp(`${ID_BOUNDARY_BEFORE}${esc}\\s*\\(`, withUnicodeFlag(flags))
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -89,7 +89,7 @@ export type {
|
|
|
89
89
|
} from './adapters/interface.ts'
|
|
90
90
|
export { JsxAdapter } from './adapters/jsx-adapter.ts'
|
|
91
91
|
export type { JsxAdapterConfig } from './adapters/jsx-adapter.ts'
|
|
92
|
-
export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
92
|
+
export { rewriteImportsForTemplate, rewriteDynamicImportsInSource } from './adapters/template-imports.ts'
|
|
93
93
|
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
94
94
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
95
95
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
@@ -5,12 +5,14 @@
|
|
|
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, anyNameIn } 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
|
+
import { extractFreeIdentifiersFromText } from './csr-substitute.ts'
|
|
12
13
|
import { walkIR, stopAt } from './walker.ts'
|
|
13
14
|
import { buildLoopChainExpr } from '../loop-chain.ts'
|
|
15
|
+
import { identifierPattern } from '../identifier-pattern.ts'
|
|
14
16
|
|
|
15
17
|
/** Expressions that render nothing (0 DOM nodes) — `&&` / `?:` empty branches. */
|
|
16
18
|
const EMPTY_RENDER_EXPRS = new Set(['null', 'undefined', 'false', "''", '""', '``'])
|
|
@@ -311,7 +313,7 @@ export function collectInnerLoops(
|
|
|
311
313
|
const template = n.children.map(c => irToPlaceholderTemplate(c, undefined, emitDepth, loopParamsForTemplate)).join('')
|
|
312
314
|
// Check if array expression references the outer loop param
|
|
313
315
|
const refsOuter = outerLoopParam
|
|
314
|
-
?
|
|
316
|
+
? identifierPattern(outerLoopParam).test(n.array)
|
|
315
317
|
: false
|
|
316
318
|
// Per-item bindings for inner loop body, collected uniformly when
|
|
317
319
|
// ctx is available: reactiveTexts / reactiveAttrs / refs are each
|
|
@@ -332,8 +334,8 @@ export function collectInnerLoops(
|
|
|
332
334
|
const innerPreambleNames = preambleNamesOf(n)
|
|
333
335
|
if (ctx) {
|
|
334
336
|
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))
|
|
337
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
|
|
338
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames, n.index))
|
|
337
339
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
338
340
|
}
|
|
339
341
|
}
|
|
@@ -375,6 +377,8 @@ export function collectInnerLoops(
|
|
|
375
377
|
siblingOffsets,
|
|
376
378
|
n.param,
|
|
377
379
|
n.paramBindings,
|
|
380
|
+
innerPreambleNames,
|
|
381
|
+
n.index,
|
|
378
382
|
))
|
|
379
383
|
}
|
|
380
384
|
}
|
|
@@ -671,7 +675,7 @@ export function collectElements(
|
|
|
671
675
|
const childHandlers: string[] = []
|
|
672
676
|
const bindings = projectionInner
|
|
673
677
|
? emptyLoopChildBindings()
|
|
674
|
-
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l))
|
|
678
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l), l.index)
|
|
675
679
|
if (!projectionInner) {
|
|
676
680
|
for (const child of l.children) {
|
|
677
681
|
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
@@ -1146,7 +1150,7 @@ function collectBranchLoops(
|
|
|
1146
1150
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1147
1151
|
// no-op for existing items.
|
|
1148
1152
|
const branchBindings = ctx && !projectionInner
|
|
1149
|
-
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n))
|
|
1153
|
+
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n), n.index)
|
|
1150
1154
|
: emptyLoopChildBindings()
|
|
1151
1155
|
|
|
1152
1156
|
loops.push({
|
|
@@ -1346,6 +1350,12 @@ export function collectLoopChildBindings(
|
|
|
1346
1350
|
* `collectLoopChildReactiveAttrs`. Omitted for a loop with no preamble.
|
|
1347
1351
|
*/
|
|
1348
1352
|
preambleNames?: ReadonlySet<string>,
|
|
1353
|
+
/**
|
|
1354
|
+
* The loop's index param name (`.map((item, i) => ...)`'s `i`), when
|
|
1355
|
+
* present — folded into the shadow guard (Copilot review on #2595) so
|
|
1356
|
+
* an index-shadowing const doesn't get const-folded either.
|
|
1357
|
+
*/
|
|
1358
|
+
loopIndex?: string | null,
|
|
1349
1359
|
): LoopChildBindings {
|
|
1350
1360
|
const bindings = emptyLoopChildBindings()
|
|
1351
1361
|
for (const child of children) {
|
|
@@ -1355,10 +1365,10 @@ export function collectLoopChildBindings(
|
|
|
1355
1365
|
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1356
1366
|
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1357
1367
|
// `.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))
|
|
1368
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex))
|
|
1369
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex))
|
|
1360
1370
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1361
|
-
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
1371
|
+
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex))
|
|
1362
1372
|
}
|
|
1363
1373
|
return bindings
|
|
1364
1374
|
}
|
|
@@ -1369,8 +1379,24 @@ export function collectLoopChildConditionals(
|
|
|
1369
1379
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1370
1380
|
loopParam?: string,
|
|
1371
1381
|
loopParamBindings?: readonly import('../types.ts').LoopParamBinding[],
|
|
1382
|
+
/**
|
|
1383
|
+
* The enclosing loop's `.map()` callback preamble locals (#2447), when
|
|
1384
|
+
* known — folded into the `expandConstantForReactivity` shadow guard
|
|
1385
|
+
* (#2482 Stage 1b) so a preamble local shadowing a component/module
|
|
1386
|
+
* const doesn't get const-folded into the condition, which would
|
|
1387
|
+
* corrupt `classifyReactivity`'s verdict below (a substituted literal
|
|
1388
|
+
* reads as "not reactive," silently freezing the branch at its initial
|
|
1389
|
+
* value instead of wiring an `insert()`).
|
|
1390
|
+
*/
|
|
1391
|
+
preambleNames?: ReadonlySet<string>,
|
|
1392
|
+
/**
|
|
1393
|
+
* The loop's index param name, when present — see
|
|
1394
|
+
* `collectLoopChildBindings`'s doc comment (Copilot review on #2595).
|
|
1395
|
+
*/
|
|
1396
|
+
loopIndex?: string | null,
|
|
1372
1397
|
): LoopChildConditional[] {
|
|
1373
1398
|
const conditionals: LoopChildConditional[] = []
|
|
1399
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
1374
1400
|
|
|
1375
1401
|
// Widen the source-level "references loop param" check so destructured
|
|
1376
1402
|
// callbacks fire too — the pattern text `[, cfg]` never word-matches on
|
|
@@ -1402,10 +1428,23 @@ export function collectLoopChildConditionals(
|
|
|
1402
1428
|
// Pre-gate using AST `reactive` flag on the source condition before
|
|
1403
1429
|
// paying for constant expansion — matches the legacy short-circuit.
|
|
1404
1430
|
if (!n.reactive && !refsLoopParamInSource) return
|
|
1405
|
-
const expanded = expandConstantForReactivity(n.condition, ctx, sourceFreeIds)
|
|
1431
|
+
const expanded = expandConstantForReactivity(n.condition, ctx, sourceFreeIds, scope)
|
|
1432
|
+
// A `.map()` callback preamble local (#2596, twin of
|
|
1433
|
+
// `collectLoopChildReactiveAttrs`'s `readsPreamble` #2447). Phase 1
|
|
1434
|
+
// already proved this condition reactive when it's set (`n.reactive`
|
|
1435
|
+
// came from `markPreambleConditionalReactivity`, gated on the local's
|
|
1436
|
+
// OWN initializer reading a signal) — `classifyReactivity` below
|
|
1437
|
+
// cannot independently confirm that: `expandConstantForReactivity`
|
|
1438
|
+
// leaves a preamble-bound identifier like `label` unexpanded on
|
|
1439
|
+
// purpose (the #2482 Stage 1b shadow guard, `scope.isBound`), so the
|
|
1440
|
+
// raw token never string-matches a signal/memo/prop pattern.
|
|
1441
|
+
const readsPreamble =
|
|
1442
|
+
preambleNames !== undefined &&
|
|
1443
|
+
preambleNames.size > 0 &&
|
|
1444
|
+
anyNameIn(expanded.freeIds ?? extractFreeIdentifiersFromText(expanded.expr), preambleNames)
|
|
1406
1445
|
// Loop-param conditionals are reactive via per-item signal accessors;
|
|
1407
1446
|
// classifyReactivity sees both paths (signal/memo/prop + loop-param).
|
|
1408
|
-
if (classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
|
|
1447
|
+
if (!readsPreamble && classifyReactivity(expanded.expr, ctx, loopParam, loopParamBindings, expanded.freeIds).kind === 'none') return
|
|
1409
1448
|
|
|
1410
1449
|
const loopParamsForCond = loopParam
|
|
1411
1450
|
? [{ param: loopParam, bindings: loopParamBindings }]
|
|
@@ -1421,9 +1460,10 @@ export function collectLoopChildConditionals(
|
|
|
1421
1460
|
condition: expanded.expr,
|
|
1422
1461
|
whenTrueHtml,
|
|
1423
1462
|
whenFalseHtml,
|
|
1424
|
-
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1425
|
-
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1463
|
+
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1464
|
+
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1426
1465
|
...(expanded.freeIds !== undefined && { conditionFreeIdentifiers: expanded.freeIds }),
|
|
1466
|
+
...(readsPreamble && { readsPreamble: true }),
|
|
1427
1467
|
})
|
|
1428
1468
|
},
|
|
1429
1469
|
})
|
|
@@ -1444,19 +1484,23 @@ function summarizeLoopChildBranch(
|
|
|
1444
1484
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1445
1485
|
loopParam?: string,
|
|
1446
1486
|
loopParamBindings?: readonly import('../types.ts').LoopParamBinding[],
|
|
1487
|
+
/** Enclosing loop's preamble locals (#2447) — see `collectLoopChildConditionals`. */
|
|
1488
|
+
preambleNames?: ReadonlySet<string>,
|
|
1489
|
+
/** Enclosing loop's index param name — see `collectLoopChildConditionals` (Copilot review on #2595). */
|
|
1490
|
+
loopIndex?: string | null,
|
|
1447
1491
|
): LoopChildBranchSummary {
|
|
1448
1492
|
const inner = collectInnerLoops([node], siblingOffsets, loopParam, ctx, branchInnerLoopOptions)
|
|
1449
1493
|
return {
|
|
1450
1494
|
childComponents: collectConditionalBranchChildComponents(node),
|
|
1451
1495
|
innerLoops: inner.length > 0 ? inner : undefined,
|
|
1452
|
-
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1496
|
+
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1453
1497
|
events: collectConditionalBranchEvents(node),
|
|
1454
1498
|
// Loop-param-aware — reuses the flat loop-item collectors scoped to just
|
|
1455
1499
|
// this branch's subtree. Both already stop descending into any further
|
|
1456
1500
|
// nested reactive conditional (own insert()/arm), so calling them here
|
|
1457
1501
|
// on the branch root yields exactly this branch's direct bindings
|
|
1458
1502
|
// without re-collecting what a nested arm already owns (#2347).
|
|
1459
|
-
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
|
|
1503
|
+
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex),
|
|
1460
1504
|
// Skip ONLY when the branch's entire content is a single bare
|
|
1461
1505
|
// `expression` (no wrapping element) that MAY yield a live DOM node —
|
|
1462
1506
|
// i.e. it contains a call anywhere (`node.hasFunctionCalls`, computed
|
|
@@ -1506,6 +1550,6 @@ function summarizeLoopChildBranch(
|
|
|
1506
1550
|
// two can't disagree on shape).
|
|
1507
1551
|
reactiveTexts: node.type === 'expression' && node.hasFunctionCalls
|
|
1508
1552
|
? []
|
|
1509
|
-
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true),
|
|
1553
|
+
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true, preambleNames, loopIndex),
|
|
1510
1554
|
}
|
|
1511
1555
|
}
|
|
@@ -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,
|
|
@@ -112,6 +112,7 @@ export function buildReactiveEffectsPlan(
|
|
|
112
112
|
whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
113
113
|
whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
114
114
|
whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
115
|
+
...(cond.readsPreamble && { readsPreamble: true }),
|
|
115
116
|
})
|
|
116
117
|
}
|
|
117
118
|
}
|
|
@@ -62,6 +62,13 @@ export interface NestedConditionalPlan {
|
|
|
62
62
|
whenFalseTemplateHtml: string
|
|
63
63
|
whenTrueArm: LoopChildArmPlan
|
|
64
64
|
whenFalseArm: LoopChildArmPlan
|
|
65
|
+
/**
|
|
66
|
+
* `wrappedCondition` reads a `.map()` callback preamble local (#2596,
|
|
67
|
+
* twin of `ReactiveAttrEffect.readsPreamble`) — the stringifier must run
|
|
68
|
+
* the row's preamble ahead of evaluating the condition getter passed to
|
|
69
|
+
* `insert()`; the local isn't otherwise in scope there.
|
|
70
|
+
*/
|
|
71
|
+
readsPreamble?: boolean
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
export interface ReactiveEffectsPlan {
|
|
@@ -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
|
}
|
|
@@ -140,7 +140,7 @@ export function stringifyReactiveEffects(
|
|
|
140
140
|
// from the attrs/texts/regions merge above (its own per-branch disposable
|
|
141
141
|
// effects), so it's untouched by row-granularity consolidation.
|
|
142
142
|
for (const cond of conditionals) {
|
|
143
|
-
emitOuterConditional(lines, indent, elVar, cond, pc)
|
|
143
|
+
emitOuterConditional(lines, indent, elVar, cond, pc, mapPreambleWrapped)
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -342,12 +342,22 @@ function emitOuterConditional(
|
|
|
342
342
|
elVar: string,
|
|
343
343
|
cond: NestedConditionalPlan,
|
|
344
344
|
pc: string | undefined,
|
|
345
|
+
mapPreambleWrapped: string | undefined,
|
|
345
346
|
): void {
|
|
346
347
|
const armIndent = `${indent} `
|
|
347
348
|
|
|
348
349
|
// Body-form arrows so live `Node` returns from Child-position
|
|
349
350
|
// interpolations route through `__bfSlot` and survive the splice (#1213).
|
|
350
|
-
|
|
351
|
+
// A condition reading a preamble local (#2596) needs the preamble re-run
|
|
352
|
+
// INSIDE the getter — `insert()` re-invokes this closure on every
|
|
353
|
+
// dependency change to decide the branch, and the local isn't otherwise in
|
|
354
|
+
// scope here (it's a plain per-row `const`, not a signal `insert()` can see
|
|
355
|
+
// through on its own). Same treatment as `readsPreamble` attrs
|
|
356
|
+
// (`emitAttrUpdate`'s callers) get ahead of their own write.
|
|
357
|
+
const conditionGetter = cond.readsPreamble && mapPreambleWrapped
|
|
358
|
+
? `() => { ${mapPreambleWrapped}; return (${cond.wrappedCondition}) }`
|
|
359
|
+
: `() => ${cond.wrappedCondition}`
|
|
360
|
+
lines.push(`${indent}insert(${elVar}, '${cond.slotId}', ${conditionGetter}, {`)
|
|
351
361
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenTrueTemplateHtml}\`, slots: __slots } },`)
|
|
352
362
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
353
363
|
stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc)
|
|
@@ -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.
|
|
@@ -728,6 +729,11 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
|
728
729
|
}
|
|
729
730
|
}
|
|
730
731
|
|
|
732
|
+
// `loopParams` here is the accessor-rewrite spec list `wrapExprWithLoopParams`
|
|
733
|
+
// consumes, not a `BindingScope`-trackable name set — see that function's
|
|
734
|
+
// docstring (`ir-to-client-js/utils.ts`) for why this stays outside #2482's
|
|
735
|
+
// migration (it's the client-JS-emitter twin of the Go adapter's
|
|
736
|
+
// `loopBindingStack`).
|
|
731
737
|
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
|
|
732
738
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
|
|
733
739
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
@@ -1572,19 +1578,26 @@ export interface TemplateOptions {
|
|
|
1572
1578
|
csrEnv?: CsrEnv
|
|
1573
1579
|
loopDepth?: number
|
|
1574
1580
|
/**
|
|
1575
|
-
*
|
|
1576
|
-
* destructured
|
|
1577
|
-
* recursion
|
|
1578
|
-
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1581
|
+
* `BindingScope` for the ENCLOSING loops' callback bindings (item /
|
|
1582
|
+
* index / destructured names / preamble locals), threaded through the
|
|
1583
|
+
* recursion one `enterLoopRow` per nesting level (#2482 Stage 1b —
|
|
1584
|
+
* migrated from the ad-hoc flat loop-bound-names `ReadonlySet<string>`
|
|
1585
|
+
* this field replaces). Inside a loop body, `csrSubstitute` receives each
|
|
1586
|
+
* expression in isolation — no enclosing arrow text — so its own
|
|
1587
|
+
* bound-name tracking can't see the loop binding; the `loop` case
|
|
1588
|
+
* instead filters these names out of the child recursion's `csrEnv`
|
|
1589
|
+
* (via `scope.boundNames()`, the SHADOW-GUARD query — see
|
|
1590
|
+
* `BindingScope.valueBoundNames`'s doc comment for why this must stay
|
|
1591
|
+
* the all-sources query, not the value-only one), so an inlinable
|
|
1592
|
+
* const / signal / memo whose name is shadowed by a loop-row binding —
|
|
1593
|
+
* including a `.map()` callback preamble local (#2447) — never
|
|
1594
|
+
* substitutes at the shadowed occurrence. Scope-accurate per nesting
|
|
1595
|
+
* level (the CURRENT level's own `transformExpr` — e.g. the loop's
|
|
1596
|
+
* array-source expression — keeps the unfiltered env). `undefined` at
|
|
1597
|
+
* the top of the recursion means "no enclosing loop" (treated as
|
|
1598
|
+
* `BindingScope.EMPTY`).
|
|
1586
1599
|
*/
|
|
1587
|
-
|
|
1600
|
+
scope?: BindingScope
|
|
1588
1601
|
/** Emit `bf-s` placeholder on scoped elements inside a jsx-children prop (#1320). */
|
|
1589
1602
|
inHoistedChildren?: boolean
|
|
1590
1603
|
/**
|
|
@@ -2226,7 +2239,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2226
2239
|
// the IR — emit does no string transformation of its own (#1277).
|
|
2227
2240
|
const source = templateExpr ?? expr
|
|
2228
2241
|
if (!source) return source
|
|
2229
|
-
const { rewritten, freeIdentifiers } = csrSubstitute(source, env)
|
|
2242
|
+
const { rewritten, freeIdentifiers } = csrSubstitute(source, env, opts.scope)
|
|
2230
2243
|
|
|
2231
2244
|
// The CSR template runs at module scope, so any post-substitution
|
|
2232
2245
|
// free identifier that lands in `unsafeLocalNames` (init-body-only
|
|
@@ -2458,23 +2471,21 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2458
2471
|
}
|
|
2459
2472
|
|
|
2460
2473
|
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)
|
|
2474
|
+
// Enter this loop's row frame and filter its bound names out of the
|
|
2475
|
+
// child recursion's substitution env (#2222, #2482 Stage 1b): an
|
|
2476
|
+
// inlinable const / signal / memo whose name the callback shadows —
|
|
2477
|
+
// item, index, a destructured binding, OR a `.map()` callback
|
|
2478
|
+
// preamble local (#2447) — must never substitute inside the body.
|
|
2479
|
+
// `enterLoopRow` mirrors `jsx-to-ir.ts`'s legacy mutable loop-param
|
|
2480
|
+
// set semantics exactly (see its doc comment): a raw pattern-text `param` (the
|
|
2481
|
+
// BF025 fallback shapes) with no `paramBindings` still gets added,
|
|
2482
|
+
// but as non-identifier pattern text it can never collide with a
|
|
2483
|
+
// real const/signal/memo name, so it's a harmless no-op for this
|
|
2484
|
+
// filter. `boundNames()` (not `valueBoundNames()`) is the correct
|
|
2485
|
+
// query here — this is a SHADOW GUARD (every binding source must
|
|
2486
|
+
// hide an outer same-named const), not a reactivity classifier.
|
|
2487
|
+
const childScope = (opts.scope ?? BindingScope.EMPTY).enterLoopRow(node)
|
|
2488
|
+
const boundHere = childScope.boundNames()
|
|
2478
2489
|
const childEnv: CsrEnv = {
|
|
2479
2490
|
...env,
|
|
2480
2491
|
substitutions: new Map([...env.substitutions].filter(([name]) => !boundHere.has(name))),
|
|
@@ -2483,7 +2494,7 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2483
2494
|
...opts,
|
|
2484
2495
|
loopDepth: loopDepth + 1,
|
|
2485
2496
|
inHoistedChildren: false,
|
|
2486
|
-
|
|
2497
|
+
scope: childScope,
|
|
2487
2498
|
csrEnv: childEnv,
|
|
2488
2499
|
})
|
|
2489
2500
|
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 }
|