@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
|
@@ -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,10 +667,14 @@ 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
|
-
* spreading — this runs per attribute (Copilot review).
|
|
627
|
-
|
|
674
|
+
* spreading — this runs per attribute (Copilot review). Exported for
|
|
675
|
+
* `collectLoopChildConditionals`'s `readsPreamble` check (#2596), the
|
|
676
|
+
* condition-position twin of the attr check just below this uses it for. */
|
|
677
|
+
export function anyNameIn(names: Iterable<string>, set: ReadonlySet<string>): boolean {
|
|
628
678
|
for (const n of names) if (set.has(n)) return true
|
|
629
679
|
return false
|
|
630
680
|
}
|
|
@@ -636,8 +686,10 @@ export function collectLoopChildReactiveAttrs(
|
|
|
636
686
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
637
687
|
stopAtReactiveConditionals = false,
|
|
638
688
|
preambleNames?: ReadonlySet<string>,
|
|
689
|
+
loopIndex?: string | null,
|
|
639
690
|
): LoopChildReactiveAttr[] {
|
|
640
691
|
const attrs: LoopChildReactiveAttr[] = []
|
|
692
|
+
const scope = buildLoopRowScope(loopParam, loopParamBindings, preambleNames, loopIndex)
|
|
641
693
|
traverseElements(node, (el) => {
|
|
642
694
|
if (el.slotId) {
|
|
643
695
|
for (const attr of el.attrs) {
|
|
@@ -653,7 +705,7 @@ export function collectLoopChildReactiveAttrs(
|
|
|
653
705
|
if (attr.name === 'key') continue
|
|
654
706
|
const valueStr = attrValueToString(attr.value)
|
|
655
707
|
if (!valueStr) continue
|
|
656
|
-
const expanded = expandConstantForReactivity(valueStr, ctx, attr.freeIdentifiers)
|
|
708
|
+
const expanded = expandConstantForReactivity(valueStr, ctx, attr.freeIdentifiers, scope)
|
|
657
709
|
// `/* @client */` always defers via per-item createEffect
|
|
658
710
|
// regardless of the reactivity classifier — matches the
|
|
659
711
|
// 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',
|
|
@@ -578,6 +578,22 @@ export interface LoopChildConditional {
|
|
|
578
578
|
* constants' own `freeIdentifiers`.
|
|
579
579
|
*/
|
|
580
580
|
conditionFreeIdentifiers?: ReadonlySet<string>
|
|
581
|
+
/**
|
|
582
|
+
* `condition` references a `.map()` callback preamble-declared name
|
|
583
|
+
* (#2596, twin of `LoopChildReactiveAttr.readsPreamble` #2447) — the
|
|
584
|
+
* emitter must run the preamble ahead of evaluating the condition getter
|
|
585
|
+
* passed to `insert()`, since the local is not otherwise in scope there.
|
|
586
|
+
* Set whenever the condition mentions ANY preamble-declared name
|
|
587
|
+
* (`preambleNamesOf`), regardless of why the conditional was classified
|
|
588
|
+
* reactive — the scope obligation is the same either way. Whether the
|
|
589
|
+
* conditional is wired reactive AT ALL is the separate, stricter Phase-1
|
|
590
|
+
* question (`IRLoop.preamble.reactiveNames` via
|
|
591
|
+
* `markPreambleConditionalReactivity`, plus the ordinary signal/prop
|
|
592
|
+
* classifiers); `classifyReactivity` can't answer it from the token
|
|
593
|
+
* alone because `expandConstantForReactivity`'s shadow guard leaves a
|
|
594
|
+
* preamble-bound identifier unexpanded on purpose (#2482 Stage 1b).
|
|
595
|
+
*/
|
|
596
|
+
readsPreamble?: boolean
|
|
581
597
|
}
|
|
582
598
|
|
|
583
599
|
export interface TopLevelLoop extends LoopCore {
|
|
@@ -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
|
)
|
|
@@ -697,6 +698,21 @@ export interface LoopParamSpec {
|
|
|
697
698
|
* avoiding post-hoc regex replacement on full template strings.
|
|
698
699
|
*
|
|
699
700
|
* Accepts either a bare param name or a spec carrying destructure bindings.
|
|
701
|
+
*
|
|
702
|
+
* #2482: this `loopParams` parameter (and the same-named param on
|
|
703
|
+
* `irToHtmlTemplate` / `irToPlaceholderTemplate` in `html-template.ts`,
|
|
704
|
+
* threaded through `collect-elements.ts` / `build-event-delegation.ts`) is
|
|
705
|
+
* NOT one of the ratchet's tracked ad-hoc scope devices, even though the
|
|
706
|
+
* ledger's textual pattern happens to match its spelling. `BindingScope`
|
|
707
|
+
* answers EXISTENCE/kind/depth queries about bound names; this ordered
|
|
708
|
+
* `ReadonlyArray<string | LoopParamSpec>` instead carries the ACCESSOR-
|
|
709
|
+
* REWRITE payload for outermost-to-innermost text substitution
|
|
710
|
+
* (`item` → `__bfItem().path`) — a rendering/codegen concern `BindingScope`
|
|
711
|
+
* has no field for by design, the client-JS-emitter twin of the Go
|
|
712
|
+
* adapter's `loopBindingStack` (see that field's docstring on
|
|
713
|
+
* `GoTemplateAdapter`). Order matters here (each nesting level's wrap
|
|
714
|
+
* composes over the previous), which is exactly what a scope EXISTENCE
|
|
715
|
+
* stack does not model.
|
|
700
716
|
*/
|
|
701
717
|
export function wrapExprWithLoopParams(expr: string, loopParams?: ReadonlyArray<string | LoopParamSpec>): string {
|
|
702
718
|
if (!loopParams) return expr
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -63,6 +63,7 @@ import { reconstructAsSegments } from './strip-types.ts'
|
|
|
63
63
|
import { templatePartsToJsExpr } from './template-parts.ts'
|
|
64
64
|
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
65
65
|
import { BindingScope } from './scope/binding-scope.ts'
|
|
66
|
+
import { identifierPattern, identifierCallPattern } from './identifier-pattern.ts'
|
|
66
67
|
|
|
67
68
|
// =============================================================================
|
|
68
69
|
// Transform Context
|
|
@@ -681,19 +682,19 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
681
682
|
patterns: {
|
|
682
683
|
signals: analyzer.signals.map(s => ({
|
|
683
684
|
getter: s.getter,
|
|
684
|
-
pattern:
|
|
685
|
+
pattern: identifierCallPattern(s.getter),
|
|
685
686
|
})),
|
|
686
687
|
memos: analyzer.memos.map(m => ({
|
|
687
688
|
name: m.name,
|
|
688
|
-
pattern:
|
|
689
|
+
pattern: identifierCallPattern(m.name),
|
|
689
690
|
})),
|
|
690
691
|
props: analyzer.propsParams
|
|
691
692
|
.filter(p => p.name !== 'children')
|
|
692
|
-
.map(p => ({ name: p.name, pattern:
|
|
693
|
+
.map(p => ({ name: p.name, pattern: identifierPattern(p.name) })),
|
|
693
694
|
constants: analyzer.localConstants.map(c => ({
|
|
694
695
|
name: c.name,
|
|
695
696
|
value: c.value,
|
|
696
|
-
pattern:
|
|
697
|
+
pattern: identifierPattern(c.name),
|
|
697
698
|
})),
|
|
698
699
|
},
|
|
699
700
|
getJS(node: ts.Node): string {
|
|
@@ -844,7 +845,7 @@ function makeBindingEnv(ctx: TransformContext): BindingEnvironment {
|
|
|
844
845
|
// mutated (cached on the immutable `BindingScope`) — a stable
|
|
845
846
|
// snapshot even if `ctx.scope` is later reassigned by an enclosing
|
|
846
847
|
// visitor frame, which swaps the instance rather than mutating it.
|
|
847
|
-
|
|
848
|
+
loopValueBoundNames: boundNames,
|
|
848
849
|
checker: a.checker,
|
|
849
850
|
}
|
|
850
851
|
ctx._bindingEnv = env
|
|
@@ -2201,7 +2202,7 @@ function transformExpressionInner(
|
|
|
2201
2202
|
// (#2482 Stage 1a Commit 2).
|
|
2202
2203
|
const scopeValueNames = ctx.scope.valueBoundNames()
|
|
2203
2204
|
const refsLoopParam = scopeValueNames.size > 0
|
|
2204
|
-
&& Array.from(scopeValueNames).some(p =>
|
|
2205
|
+
&& Array.from(scopeValueNames).some(p => identifierPattern(p).test(exprText))
|
|
2205
2206
|
|
|
2206
2207
|
// Compute AST-derived flags. `callsReactive` recognises signal-getter / memo
|
|
2207
2208
|
// calls even inside deeper expressions (e.g., `format(count())`); `hasCalls`
|
|
@@ -2267,7 +2268,11 @@ function transformJsxFunctionCall(
|
|
|
2267
2268
|
const substitutedGetJS = (node: ts.Node) => {
|
|
2268
2269
|
let text = baseGetJS(node)
|
|
2269
2270
|
for (const [paramName, argExpr] of substitutions) {
|
|
2270
|
-
|
|
2271
|
+
// Replacer function, not the string form: `argExpr` is arbitrary
|
|
2272
|
+
// caller-supplied JS text and may itself contain `$` sequences
|
|
2273
|
+
// (`$&`, `$1`, …) that `String.replace`'s string-replacement form
|
|
2274
|
+
// would interpret specially instead of inserting literally (#2592).
|
|
2275
|
+
text = text.replace(identifierPattern(paramName, 'g'), () => argExpr)
|
|
2271
2276
|
}
|
|
2272
2277
|
return text
|
|
2273
2278
|
}
|
|
@@ -2319,7 +2324,11 @@ function transformMultiReturnJsxFunctionCall(
|
|
|
2319
2324
|
const substitutedGetJS = (node: ts.Node) => {
|
|
2320
2325
|
let text = baseGetJS(node)
|
|
2321
2326
|
for (const [paramName, argExpr] of substitutions) {
|
|
2322
|
-
|
|
2327
|
+
// Replacer function, not the string form: `argExpr` is arbitrary
|
|
2328
|
+
// caller-supplied JS text and may itself contain `$` sequences
|
|
2329
|
+
// (`$&`, `$1`, …) that `String.replace`'s string-replacement form
|
|
2330
|
+
// would interpret specially instead of inserting literally (#2592).
|
|
2331
|
+
text = text.replace(identifierPattern(paramName, 'g'), () => argExpr)
|
|
2323
2332
|
}
|
|
2324
2333
|
return text
|
|
2325
2334
|
}
|
|
@@ -5012,6 +5021,13 @@ function transformMapCall(
|
|
|
5012
5021
|
// the row effect re-runs the preamble ahead of the write.
|
|
5013
5022
|
if (preamble && !isStaticArray) {
|
|
5014
5023
|
markPreambleAttrSlots(children, new Set(preamble.declaredNames), ctx)
|
|
5024
|
+
// #2596 follow-up to the two passes above — the CONDITION-position twin.
|
|
5025
|
+
// See `markPreambleConditionalReactivity`'s docstring for why this one is
|
|
5026
|
+
// gated on `reactiveNames` (genuinely signal-derived) rather than the
|
|
5027
|
+
// blanket `declaredNames` the region/attr passes use.
|
|
5028
|
+
if (preamble.reactiveNames && preamble.reactiveNames.length > 0) {
|
|
5029
|
+
markPreambleConditionalReactivity(children, new Set(preamble.reactiveNames), ctx)
|
|
5030
|
+
}
|
|
5015
5031
|
}
|
|
5016
5032
|
|
|
5017
5033
|
// Collect nested components for both static and dynamic arrays.
|
|
@@ -5448,6 +5464,67 @@ function markPreambleAttrSlots(
|
|
|
5448
5464
|
visit(nodes)
|
|
5449
5465
|
}
|
|
5450
5466
|
|
|
5467
|
+
/**
|
|
5468
|
+
* Grant the IR `reactive` flag (and a slot id) to a loop-body CONDITIONAL
|
|
5469
|
+
* whose condition bare-references a preamble local that is itself reactive
|
|
5470
|
+
* (#2596) — `reactiveNames`, computed by `computePreambleReactiveNames`.
|
|
5471
|
+
*
|
|
5472
|
+
* Root cause this closes: a plain (non-preamble) condition's `reactive` flag
|
|
5473
|
+
* comes from `isReactiveExpression`/`isReactiveOrigin` run against the
|
|
5474
|
+
* condition's OWN text at `transformConditional`/`transformLogicalAnd`/the
|
|
5475
|
+
* if-else-chain builder — none of which know about an enclosing loop's
|
|
5476
|
+
* `.map()` callback preamble (that's assembled and only becomes available
|
|
5477
|
+
* on `IRLoop` after `children` is already built, same ordering constraint
|
|
5478
|
+
* `collectPreambleRegions`/`markPreambleAttrSlots` work around). A condition
|
|
5479
|
+
* that's just a bare reference to a preamble local (`label ? <A/> : <B/>`)
|
|
5480
|
+
* therefore falls through every one of those classifiers with `reactive:
|
|
5481
|
+
* false` even when `label`'s initializer reads a signal — the classifiers
|
|
5482
|
+
* only ever see the token `label`, never its declaration.
|
|
5483
|
+
*
|
|
5484
|
+
* Deliberately narrower than `collectPreambleRegions`/`markPreambleAttrSlots`:
|
|
5485
|
+
* those two mark ANY reference to `declaredNames` because their region-patch
|
|
5486
|
+
* effect re-runs the WHOLE preamble unconditionally on row update, so a
|
|
5487
|
+
* merely item-derived local (`const label = item.title`) still ends up
|
|
5488
|
+
* correctly tracked by ordinary signal auto-tracking inside that effect —
|
|
5489
|
+
* marking it reactive there costs nothing extra. A conditional is different:
|
|
5490
|
+
* `reactive: true` here means Phase 2 wraps it in an `insert()` that swaps
|
|
5491
|
+
* whole DOM subtrees, which is real cost and, for an item-only local, pure
|
|
5492
|
+
* waste — the row already renders fresh whenever it's (re)constructed. So
|
|
5493
|
+
* this only fires for `reactiveNames`, the subset PROVEN to depend on an
|
|
5494
|
+
* actual external signal/memo/prop.
|
|
5495
|
+
*
|
|
5496
|
+
* Mirrors the sibling passes' traversal shape (conditional arms included).
|
|
5497
|
+
*/
|
|
5498
|
+
function markPreambleConditionalReactivity(
|
|
5499
|
+
nodes: IRNode[],
|
|
5500
|
+
reactiveNames: ReadonlySet<string>,
|
|
5501
|
+
ctx: TransformContext,
|
|
5502
|
+
): void {
|
|
5503
|
+
if (reactiveNames.size === 0) return
|
|
5504
|
+
const visit = (list: IRNode[]): void => {
|
|
5505
|
+
for (const node of list) {
|
|
5506
|
+
switch (node.type) {
|
|
5507
|
+
case 'element':
|
|
5508
|
+
case 'fragment':
|
|
5509
|
+
visit(node.children)
|
|
5510
|
+
break
|
|
5511
|
+
case 'conditional': {
|
|
5512
|
+
if (!node.reactive) {
|
|
5513
|
+
const refs = extractFreeIdentifiersFromText(node.condition)
|
|
5514
|
+
if ([...refs].some((r) => reactiveNames.has(r))) {
|
|
5515
|
+
node.reactive = true
|
|
5516
|
+
if (!node.slotId) node.slotId = generateSlotId(ctx)
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
visit([node.whenTrue, ...(node.whenFalse ? [node.whenFalse] : [])])
|
|
5520
|
+
break
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
visit(nodes)
|
|
5526
|
+
}
|
|
5527
|
+
|
|
5451
5528
|
/**
|
|
5452
5529
|
* Expression-bearing source text of an attribute value, for the
|
|
5453
5530
|
* free-identifier scan. Only used as a FALLBACK — an attribute normally
|
|
@@ -5494,6 +5571,52 @@ function collectPreambleDeclaredNames(stmt: ts.Statement, out: Set<string>): voi
|
|
|
5494
5571
|
}
|
|
5495
5572
|
}
|
|
5496
5573
|
|
|
5574
|
+
/**
|
|
5575
|
+
* Which {@link MapCallbackPreamble.declaredNames} are themselves reactive —
|
|
5576
|
+
* their OWN initializer reads a signal / memo / reactive prop, directly or
|
|
5577
|
+
* transitively through an earlier declaration in the same preamble (#2596).
|
|
5578
|
+
*
|
|
5579
|
+
* Reuses the same Phase-1 classifier (`isReactiveExpression`) already applied
|
|
5580
|
+
* to every non-preamble condition/attribute — this doesn't invent a second
|
|
5581
|
+
* reactivity heuristic, it runs the existing one over one more declaration
|
|
5582
|
+
* list. `ctx.patterns.constants` (the component-level constant chain
|
|
5583
|
+
* `isSignalOrMemoReference`/`isPropsReference` already walk) doesn't reach
|
|
5584
|
+
* these names — they're scoped to the `.map()` callback body, never added to
|
|
5585
|
+
* `ctx.patterns` — so without this pass a preamble local reading a signal is
|
|
5586
|
+
* invisible to the classifier.
|
|
5587
|
+
*
|
|
5588
|
+
* Value-declaration statements only (`const`/`let x = expr`, including
|
|
5589
|
+
* destructuring targets, which mark every bound name reactive together since
|
|
5590
|
+
* they share one initializer). A non-declaration preamble statement
|
|
5591
|
+
* (assignment, loop, side-effecting call) contributes no reactive names —
|
|
5592
|
+
* consistent with `neutralPreambleDeclarations`'s all-or-nothing DSL gate
|
|
5593
|
+
* treating anything past that as un-lowerable, though this walk (client/JS-
|
|
5594
|
+
* runtime SSR only) doesn't require the same all-or-nothing: it simply skips
|
|
5595
|
+
* what it can't classify.
|
|
5596
|
+
*/
|
|
5597
|
+
function computePreambleReactiveNames(
|
|
5598
|
+
statements: readonly ts.Statement[],
|
|
5599
|
+
ctx: TransformContext,
|
|
5600
|
+
): Set<string> {
|
|
5601
|
+
const reactiveNames = new Set<string>()
|
|
5602
|
+
for (const stmt of statements) {
|
|
5603
|
+
if (!ts.isVariableStatement(stmt)) continue
|
|
5604
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
5605
|
+
if (!decl.initializer) continue
|
|
5606
|
+
const boundNames = new Set<string>()
|
|
5607
|
+
collectBindingNames(decl.name, boundNames)
|
|
5608
|
+
const initText = ctx.getJS(decl.initializer)
|
|
5609
|
+
const initFreeRefs = extractFreeIdentifiersFromNode(decl.initializer)
|
|
5610
|
+
const readsEarlierReactive = [...initFreeRefs].some((r) => reactiveNames.has(r))
|
|
5611
|
+
const isReactive = readsEarlierReactive || isReactiveExpression(initText, ctx, decl.initializer)
|
|
5612
|
+
if (isReactive) {
|
|
5613
|
+
for (const n of boundNames) reactiveNames.add(n)
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
return reactiveNames
|
|
5618
|
+
}
|
|
5619
|
+
|
|
5497
5620
|
/**
|
|
5498
5621
|
* Build a {@link MapCallbackPreamble} from value-only (JSX-free) pre-return
|
|
5499
5622
|
* statements — the Stage-2 fold preamble and the plain block-body preamble.
|
|
@@ -5517,6 +5640,7 @@ function preambleFromValueStatements(
|
|
|
5517
5640
|
typedParts.push(raw0.endsWith(';') ? raw0 : raw0 + ';')
|
|
5518
5641
|
segments.push(tjs !== js ? { kind: 'js', text: js, templateText: tjs } : { kind: 'js', text: js })
|
|
5519
5642
|
}
|
|
5643
|
+
const reactiveNames = computePreambleReactiveNames(statements, ctx)
|
|
5520
5644
|
return {
|
|
5521
5645
|
segments: trimPreambleSegments(segments),
|
|
5522
5646
|
ssrText: tsxSourceText(typedParts.join(' ')),
|
|
@@ -5524,6 +5648,7 @@ function preambleFromValueStatements(
|
|
|
5524
5648
|
// Value-only preambles accumulate no JSX, so no child needs the array join.
|
|
5525
5649
|
builderNames: [],
|
|
5526
5650
|
declarations: neutralPreambleDeclarations(statements, ctx) ?? undefined,
|
|
5651
|
+
reactiveNames: reactiveNames.size > 0 ? [...reactiveNames] : undefined,
|
|
5527
5652
|
}
|
|
5528
5653
|
}
|
|
5529
5654
|
|
|
@@ -7104,7 +7229,7 @@ function referencesLoopParam(expr: string, ctx: TransformContext): boolean {
|
|
|
7104
7229
|
const boundNames = ctx.scope.valueBoundNames()
|
|
7105
7230
|
if (boundNames.size === 0) return false
|
|
7106
7231
|
for (const p of boundNames) {
|
|
7107
|
-
if (
|
|
7232
|
+
if (identifierPattern(p).test(expr)) return true
|
|
7108
7233
|
}
|
|
7109
7234
|
return false
|
|
7110
7235
|
}
|
|
@@ -7238,7 +7363,7 @@ function hasReactiveAttributes(attrs: IRAttribute[], ctx: TransformContext): boo
|
|
|
7238
7363
|
const scopeValueNames = ctx.scope.valueBoundNames()
|
|
7239
7364
|
if (scopeValueNames.size > 0) {
|
|
7240
7365
|
for (const p of scopeValueNames) {
|
|
7241
|
-
if (
|
|
7366
|
+
if (identifierPattern(p).test(valueToCheck)) return true
|
|
7242
7367
|
}
|
|
7243
7368
|
}
|
|
7244
7369
|
}
|
package/src/module-exports.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* This is a compiler-layer concern, not adapter-specific.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import ts from 'typescript'
|
|
8
9
|
import type { ComponentIR, ParamInfo } from './types.ts'
|
|
10
|
+
import { identifierPattern } from './identifier-pattern.ts'
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Emit module-level exports for local declarations and `export { ... } [from '...']`
|
|
@@ -140,7 +142,7 @@ export function findReachableNames(
|
|
|
140
142
|
const queue: string[] = []
|
|
141
143
|
|
|
142
144
|
for (const name of allNames) {
|
|
143
|
-
if (
|
|
145
|
+
if (identifierPattern(name).test(primaryRefs)) {
|
|
144
146
|
reachable.add(name)
|
|
145
147
|
queue.push(name)
|
|
146
148
|
}
|
|
@@ -150,7 +152,7 @@ export function findReachableNames(
|
|
|
150
152
|
const current = queue.shift()!
|
|
151
153
|
const body = bodyMap.get(current) || ''
|
|
152
154
|
for (const name of allNames) {
|
|
153
|
-
if (!reachable.has(name) &&
|
|
155
|
+
if (!reachable.has(name) && identifierPattern(name).test(body)) {
|
|
154
156
|
reachable.add(name)
|
|
155
157
|
queue.push(name)
|
|
156
158
|
}
|
|
@@ -160,6 +162,142 @@ export function findReachableNames(
|
|
|
160
162
|
return reachable
|
|
161
163
|
}
|
|
162
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Which of `candidates` does `bodyText` ASSIGN to?
|
|
167
|
+
*
|
|
168
|
+
* Reachability above answers "is this declaration referenced?", which is
|
|
169
|
+
* the right question for pruning SSR-irrelevant code. It is the wrong
|
|
170
|
+
* question for a MUTABLE binding: a surviving `let` whose only writer got
|
|
171
|
+
* pruned is left declared-and-read but never assigned, and TypeScript's
|
|
172
|
+
* control-flow analysis then narrows it to `never` at every guarded use
|
|
173
|
+
* (#2598). `closeOverWritersOfMutableBindings` uses this to restore the
|
|
174
|
+
* missing half of that pair.
|
|
175
|
+
*
|
|
176
|
+
* Recognizes the forms that actually write a local binding:
|
|
177
|
+
* `x = …`, `x += …` (and every other compound operator), `x++`, `--x`
|
|
178
|
+
* Destructuring assignment (`[x] = …`, `({ x } = …)`) is deliberately NOT
|
|
179
|
+
* recognized: it never appears in the ref/handler shapes this exists for,
|
|
180
|
+
* and a wrong guess here over-retains rather than fails loudly, so leaving
|
|
181
|
+
* it out keeps the retained set honest. If one shows up, it will present
|
|
182
|
+
* as this same `never` narrowing and can be added with a fixture.
|
|
183
|
+
*
|
|
184
|
+
* Parsed with the TS AST, not matched as text: `identifierPattern` (used
|
|
185
|
+
* for reference detection above) cannot tell a write from a read, and a
|
|
186
|
+
* regex for `name\s*=` would match `name == x`, a `name=` inside a string
|
|
187
|
+
* or JSX attribute, and a property write `obj.name = x` that assigns
|
|
188
|
+
* nothing of the sort.
|
|
189
|
+
*/
|
|
190
|
+
export function findAssignedNames(
|
|
191
|
+
bodyText: string,
|
|
192
|
+
candidates: ReadonlySet<string>,
|
|
193
|
+
): Set<string> {
|
|
194
|
+
const assigned = new Set<string>()
|
|
195
|
+
if (candidates.size === 0) return assigned
|
|
196
|
+
|
|
197
|
+
const sf = ts.createSourceFile(
|
|
198
|
+
'bf-assignment-scan.tsx',
|
|
199
|
+
bodyText,
|
|
200
|
+
ts.ScriptTarget.Latest,
|
|
201
|
+
/* setParentNodes */ false,
|
|
202
|
+
ts.ScriptKind.TSX,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// A bare Identifier on the left of an assignment — `obj.x = …` is a
|
|
206
|
+
// PropertyAccessExpression and writes through the binding rather than to
|
|
207
|
+
// it, so it does not count.
|
|
208
|
+
const record = (target: ts.Node): void => {
|
|
209
|
+
if (ts.isIdentifier(target) && candidates.has(target.text)) {
|
|
210
|
+
assigned.add(target.text)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const visit = (node: ts.Node): void => {
|
|
215
|
+
if (ts.isBinaryExpression(node) && isAssignmentOperator(node.operatorToken.kind)) {
|
|
216
|
+
record(node.left)
|
|
217
|
+
} else if (
|
|
218
|
+
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
219
|
+
(node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
220
|
+
) {
|
|
221
|
+
record(node.operand)
|
|
222
|
+
}
|
|
223
|
+
ts.forEachChild(node, visit)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ts.forEachChild(sf, visit)
|
|
227
|
+
return assigned
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `findReachableNames`, plus the invariant it cannot express on its own:
|
|
232
|
+
* **a mutable binding that survives keeps the declarations that write it.**
|
|
233
|
+
*
|
|
234
|
+
* Reachability is seeded from the RENDERED JSX, which has already had the
|
|
235
|
+
* client-only attributes stripped — `ref={setRef}` leaves no `setRef`
|
|
236
|
+
* behind, and `onClick={handleClick}` is rendered as `onClick={() => {}}`.
|
|
237
|
+
* That is deliberate: code reachable only from a handler is client-only
|
|
238
|
+
* and should not be emitted into an SSR template.
|
|
239
|
+
*
|
|
240
|
+
* It goes wrong when a `let` outlives its writer. The binding survives
|
|
241
|
+
* because some OTHER surviving declaration reads it, while its only
|
|
242
|
+
* assignment lived in a pruned handler — so the emitted template declares
|
|
243
|
+
* it, reads it, and never assigns it. TypeScript's control-flow analysis
|
|
244
|
+
* concludes it is permanently `null`, narrows every guarded use to `never`,
|
|
245
|
+
* and each member access on it fails:
|
|
246
|
+
*
|
|
247
|
+
* let highlightEl: HTMLElement | null = null // writer was pruned
|
|
248
|
+
* const syncScroll = () => {
|
|
249
|
+
* if (highlightEl && textareaEl) {
|
|
250
|
+
* highlightEl.scrollTop = textareaEl.scrollTop // TS2339 on `never`
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
*
|
|
254
|
+
* Pulling the writers back in restores the source's shape for exactly the
|
|
255
|
+
* bindings that survived — nothing else. The retained writer is dead code
|
|
256
|
+
* at SSR (it only ever runs from a hydrated event), which is the same
|
|
257
|
+
* harmless-unused-declaration trade `generateModuleScopeDeclarations`
|
|
258
|
+
* already makes deliberately.
|
|
259
|
+
*
|
|
260
|
+
* Iterates to a fixpoint because a newly retained writer can read further
|
|
261
|
+
* declarations, and can itself write another mutable binding. Bounded by
|
|
262
|
+
* the declaration count: each round either adds a name or stops.
|
|
263
|
+
*/
|
|
264
|
+
export function closeOverWritersOfMutableBindings(
|
|
265
|
+
primaryRefs: string,
|
|
266
|
+
declarations: { name: string; body: string }[],
|
|
267
|
+
mutableNames: ReadonlySet<string>,
|
|
268
|
+
): Set<string> {
|
|
269
|
+
let reachable = findReachableNames(primaryRefs, declarations)
|
|
270
|
+
if (mutableNames.size === 0) return reachable
|
|
271
|
+
|
|
272
|
+
let seedText = primaryRefs
|
|
273
|
+
for (let round = 0; round <= declarations.length; round++) {
|
|
274
|
+
const survivingMutables = new Set(
|
|
275
|
+
[...reachable].filter(name => mutableNames.has(name)),
|
|
276
|
+
)
|
|
277
|
+
if (survivingMutables.size === 0) return reachable
|
|
278
|
+
|
|
279
|
+
const added = declarations
|
|
280
|
+
.filter(d => !reachable.has(d.name))
|
|
281
|
+
.filter(d => findAssignedNames(d.body, survivingMutables).size > 0)
|
|
282
|
+
.map(d => d.name)
|
|
283
|
+
if (added.length === 0) return reachable
|
|
284
|
+
|
|
285
|
+
// Re-seed by NAME rather than merging sets directly, so each retained
|
|
286
|
+
// writer's own transitive dependencies come along through the same
|
|
287
|
+
// traversal instead of a second, divergent one.
|
|
288
|
+
seedText += '\n' + added.join('\n')
|
|
289
|
+
reachable = findReachableNames(seedText, declarations)
|
|
290
|
+
}
|
|
291
|
+
return reachable
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
|
295
|
+
return (
|
|
296
|
+
kind >= ts.SyntaxKind.FirstAssignment &&
|
|
297
|
+
kind <= ts.SyntaxKind.LastAssignment
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
163
301
|
/**
|
|
164
302
|
* Extract parameter names from a function expression string.
|
|
165
303
|
* Handles: arrow functions, single-param arrows, function expressions.
|
package/src/relocate.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from './adapters/interface.ts'
|
|
23
23
|
import { tsNodeToParsedExpr, type ParsedExpr } from './expression-parser.ts'
|
|
24
24
|
import { prepareLoweringMatchers, type LoweringMatcher } from './lowering-registry.ts'
|
|
25
|
+
import { identifierPattern } from './identifier-pattern.ts'
|
|
25
26
|
|
|
26
27
|
export interface RelocateEnv {
|
|
27
28
|
/**
|
|
@@ -713,7 +714,7 @@ function scanRefsByName(
|
|
|
713
714
|
): Map<string, ts.Identifier[]> {
|
|
714
715
|
const result = new Map<string, ts.Identifier[]>()
|
|
715
716
|
for (const name of bindings.keys()) {
|
|
716
|
-
const re =
|
|
717
|
+
const re = identifierPattern(name)
|
|
717
718
|
if (re.test(text)) result.set(name, [])
|
|
718
719
|
}
|
|
719
720
|
return result
|