@barefootjs/jsx 0.31.4 → 0.31.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/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/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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +773 -649
- package/dist/ir-to-client-js/client-only-elision.d.ts +11 -5
- package/dist/ir-to-client-js/client-only-elision.d.ts.map +1 -1
- package/dist/ir-to-client-js/collect-elements.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/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +16 -0
- package/dist/ir-to-client-js/reactivity.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/module-exports.d.ts +64 -0
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +1 -1
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +117 -17
- 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 +10 -1
- package/src/__tests__/doc-examples.test.ts +1 -0
- package/src/__tests__/free-refs.test.ts +1 -1
- package/src/__tests__/mutable-binding-writers.test.ts +133 -0
- package/src/__tests__/preamble-conditional-reactivity.test.ts +191 -0
- package/src/__tests__/signal-setter-updater-type.test.ts +81 -0
- package/src/adapters/jsx-adapter.ts +29 -3
- package/src/adapters/loop-bound-names.ts +18 -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 +34 -21
- package/src/free-refs.ts +14 -5
- package/src/index.ts +6 -0
- package/src/ir-to-client-js/client-only-elision.ts +11 -5
- package/src/ir-to-client-js/collect-elements.ts +17 -2
- 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/reactive-effects.ts +12 -2
- package/src/ir-to-client-js/html-template.ts +51 -0
- package/src/ir-to-client-js/reactivity.ts +4 -2
- package/src/ir-to-client-js/types.ts +16 -0
- package/src/ir-to-client-js/utils.ts +15 -0
- package/src/jsx-to-ir.ts +117 -1
- package/src/module-exports.ts +137 -0
- package/src/scope/binding-scope.ts +1 -1
- package/src/types.ts +118 -0
package/src/debug.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { WrapReason } from './ir-to-client-js/reactivity.ts'
|
|
|
30
30
|
import { decideWrapFromAstFlags } from './ir-to-client-js/reactivity.ts'
|
|
31
31
|
import { tokenContainsIdent } from './ir-to-client-js/utils.ts'
|
|
32
32
|
import { identifierCallPattern } from './identifier-pattern.ts'
|
|
33
|
+
import { BindingScope } from './scope/binding-scope.ts'
|
|
33
34
|
|
|
34
35
|
// =============================================================================
|
|
35
36
|
// Types
|
|
@@ -360,7 +361,7 @@ export function buildGraphFromIR(ir: ComponentIR): ComponentGraph {
|
|
|
360
361
|
|
|
361
362
|
// Collect DOM bindings from IR tree
|
|
362
363
|
const domBindings: DomBinding[] = []
|
|
363
|
-
collectDomBindings(ir.root, domBindings, signalGetters, memoNames, undefined,
|
|
364
|
+
collectDomBindings(ir.root, domBindings, signalGetters, memoNames, undefined, BindingScope.EMPTY, exprReadsProp)
|
|
364
365
|
|
|
365
366
|
// Build consumer lists for signals
|
|
366
367
|
const signalConsumers = new Map<string, string[]>()
|
|
@@ -1676,30 +1677,41 @@ function collectDomBindings(
|
|
|
1676
1677
|
signalGetters: Set<string>,
|
|
1677
1678
|
memoNames: Set<string>,
|
|
1678
1679
|
parentTag?: string,
|
|
1679
|
-
// Loop-param names in scope (#1690, #1795 Phase 2
|
|
1680
|
+
// Loop-param names in scope (#1690, #1795 Phase 2; threaded via the shared
|
|
1681
|
+
// `BindingScope` service since #2482 Stage 4). Inside a `map(it => …)`
|
|
1680
1682
|
// body the emitter rewrites every `it.x` read into a reactive accessor and
|
|
1681
1683
|
// wraps the binding in `createEffect`, yet `it` is neither a signal nor a
|
|
1682
1684
|
// memo — so without this context loop-child text / attribute bindings are
|
|
1683
1685
|
// invisible to the graph. When a binding expression references one of these
|
|
1684
1686
|
// names it is treated as reactive (matching the emitter's gate), giving the
|
|
1685
1687
|
// profiler a `domBinding` (slotId + loc) to resolve `<Comp>#binding:<slotId>`.
|
|
1686
|
-
|
|
1688
|
+
// This is the REACTIVITY / SLOT-ID CLASSIFIER consumer class (see
|
|
1689
|
+
// `BindingScope.valueBoundNames`'s docstring) — reads `valueBoundNames()`,
|
|
1690
|
+
// not `boundNames()`.
|
|
1691
|
+
scope: BindingScope = BindingScope.EMPTY,
|
|
1687
1692
|
// Predicate: does an attribute expression read a component prop? Mirrors the
|
|
1688
1693
|
// emitter's `needsEffectWrapper` prop detection so a prop-driven attribute
|
|
1689
1694
|
// (wrapped in `createEffect` at codegen, hence emitting `#binding:<slot>`) is
|
|
1690
1695
|
// tracked here too — otherwise its profiler id resolves to `(unresolved)`.
|
|
1691
1696
|
readsProp: (expr: string, freeIds?: ReadonlySet<string>) => boolean = () => false,
|
|
1692
1697
|
): void {
|
|
1698
|
+
const boundNames = scope.valueBoundNames()
|
|
1693
1699
|
// Does a loop-child binding read a loop param (or index)? Use the analyzer's
|
|
1694
1700
|
// lexer-resolved metadata, NOT a raw-string regex — so a param name that only
|
|
1695
1701
|
// appears inside a string literal (index `i` vs `'i'`) is not mistaken for a
|
|
1696
1702
|
// reactive read. Text expressions carry `origin.freeRefs` (a `render-item`
|
|
1697
1703
|
// kind == map-callback param); attributes carry `freeIdentifiers` (bare
|
|
1698
1704
|
// identifier set). This matches the emitter's actual loop-param gate.
|
|
1705
|
+
// Set-intersection test without spreading into an array — these run per
|
|
1706
|
+
// node in the graph walk, so avoid the per-call allocation.
|
|
1707
|
+
const setSomeIn = (names: ReadonlySet<string>, other: ReadonlySet<string>): boolean => {
|
|
1708
|
+
for (const n of names) if (other.has(n)) return true
|
|
1709
|
+
return false
|
|
1710
|
+
}
|
|
1699
1711
|
const exprReadsLoopParam = (n: IRExpression): boolean =>
|
|
1700
|
-
|
|
1712
|
+
boundNames.size > 0 && (n.origin?.freeRefs?.some(r => boundNames.has(r.name)) ?? false)
|
|
1701
1713
|
const attrReadsLoopParam = (free: ReadonlySet<string> | undefined): boolean =>
|
|
1702
|
-
|
|
1714
|
+
boundNames.size > 0 && free !== undefined && setSomeIn(boundNames, free)
|
|
1703
1715
|
switch (node.type) {
|
|
1704
1716
|
case 'element': {
|
|
1705
1717
|
// Dynamic attribute bindings (style, class, aria-*, data-*, etc.)
|
|
@@ -1713,7 +1725,7 @@ function collectDomBindings(
|
|
|
1713
1725
|
// `key` is consumed by the loop's keyFn, never emitted as an attribute
|
|
1714
1726
|
// effect — skip it inside loops so a `key={it.id}` read isn't mistaken
|
|
1715
1727
|
// for a reactive binding (matches `collectLoopChildBindings`).
|
|
1716
|
-
if (attr.name === 'key' &&
|
|
1728
|
+
if (attr.name === 'key' && boundNames.size > 0) continue
|
|
1717
1729
|
const expr = attrValueToString(attr.value)
|
|
1718
1730
|
if (!expr) continue
|
|
1719
1731
|
const deps = extractReactiveDeps(expr, signalGetters, memoNames)
|
|
@@ -1756,7 +1768,7 @@ function collectDomBindings(
|
|
|
1756
1768
|
}
|
|
1757
1769
|
// Recurse — pass element tag as parent context for text bindings
|
|
1758
1770
|
for (const child of node.children) {
|
|
1759
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, node.tag,
|
|
1771
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, node.tag, scope, readsProp)
|
|
1760
1772
|
}
|
|
1761
1773
|
break
|
|
1762
1774
|
}
|
|
@@ -1794,7 +1806,7 @@ function collectDomBindings(
|
|
|
1794
1806
|
// (the emitter wraps its `insert()` in a per-item effect) even though the
|
|
1795
1807
|
// param is neither signal nor memo. Use the resolved `origin.freeRefs`.
|
|
1796
1808
|
const loopReactive =
|
|
1797
|
-
|
|
1809
|
+
boundNames.size > 0 && (node.origin?.freeRefs?.some(r => boundNames.has(r.name)) ?? false)
|
|
1798
1810
|
if ((decision.wrap || loopReactive) && node.slotId) {
|
|
1799
1811
|
const deps = extractReactiveDeps(node.condition, signalGetters, memoNames)
|
|
1800
1812
|
bindings.push({
|
|
@@ -1813,8 +1825,8 @@ function collectDomBindings(
|
|
|
1813
1825
|
jsxPreview: `{${truncateExpr(node.condition)} ? ... : ...}`,
|
|
1814
1826
|
})
|
|
1815
1827
|
}
|
|
1816
|
-
collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag,
|
|
1817
|
-
collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag,
|
|
1828
|
+
collectDomBindings(node.whenTrue, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1829
|
+
collectDomBindings(node.whenFalse, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1818
1830
|
break
|
|
1819
1831
|
}
|
|
1820
1832
|
case 'loop': {
|
|
@@ -1828,9 +1840,9 @@ function collectDomBindings(
|
|
|
1828
1840
|
// An inner loop whose array reads an outer loop param (`r.tags.map(...)`)
|
|
1829
1841
|
// is reactive per item — use the resolved `arrayFreeIdentifiers`.
|
|
1830
1842
|
const loopReactive =
|
|
1831
|
-
|
|
1843
|
+
boundNames.size > 0 &&
|
|
1832
1844
|
node.arrayFreeIdentifiers !== undefined &&
|
|
1833
|
-
|
|
1845
|
+
setSomeIn(boundNames, node.arrayFreeIdentifiers)
|
|
1834
1846
|
const isReactive = deps.length > 0 || node.callsReactiveGetters === true || loopReactive
|
|
1835
1847
|
const isFallback = !isReactive && node.hasFunctionCalls === true
|
|
1836
1848
|
if (isReactive || isFallback) {
|
|
@@ -1859,12 +1871,13 @@ function collectDomBindings(
|
|
|
1859
1871
|
})
|
|
1860
1872
|
}
|
|
1861
1873
|
}
|
|
1862
|
-
// Loop-param names enter scope for the children (#1690, #1795 Phase 2)
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1874
|
+
// Loop-param names enter scope for the children (#1690, #1795 Phase 2),
|
|
1875
|
+
// via the same `BindingScope.enterLoopRow` the emitter uses — `IRLoop`
|
|
1876
|
+
// already satisfies `LoopBindingSource` structurally (`param`/`index`/
|
|
1877
|
+
// `paramBindings`/`preamble`), so no bespoke Set bookkeeping is needed.
|
|
1878
|
+
const childScope = scope.enterLoopRow(node)
|
|
1866
1879
|
for (const child of node.children) {
|
|
1867
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag,
|
|
1880
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, childScope, readsProp)
|
|
1868
1881
|
}
|
|
1869
1882
|
break
|
|
1870
1883
|
}
|
|
@@ -1903,21 +1916,21 @@ function collectDomBindings(
|
|
|
1903
1916
|
}
|
|
1904
1917
|
}
|
|
1905
1918
|
for (const child of node.children) {
|
|
1906
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag,
|
|
1919
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1907
1920
|
}
|
|
1908
1921
|
break
|
|
1909
1922
|
}
|
|
1910
1923
|
case 'fragment':
|
|
1911
1924
|
case 'provider': {
|
|
1912
1925
|
for (const child of node.children) {
|
|
1913
|
-
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag,
|
|
1926
|
+
collectDomBindings(child, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1914
1927
|
}
|
|
1915
1928
|
break
|
|
1916
1929
|
}
|
|
1917
1930
|
case 'if-statement': {
|
|
1918
|
-
collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag,
|
|
1931
|
+
collectDomBindings(node.consequent, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1919
1932
|
if (node.alternate) {
|
|
1920
|
-
collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag,
|
|
1933
|
+
collectDomBindings(node.alternate, bindings, signalGetters, memoNames, parentTag, scope, readsProp)
|
|
1921
1934
|
}
|
|
1922
1935
|
break
|
|
1923
1936
|
}
|
package/src/free-refs.ts
CHANGED
|
@@ -41,14 +41,23 @@ export interface BindingEnvironment {
|
|
|
41
41
|
localFunctions: readonly FunctionInfo[]
|
|
42
42
|
imports: readonly ImportInfo[]
|
|
43
43
|
ambientGlobals: ReadonlySet<string>
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Active `.map()` callback VALUE-bound names (item/index/destructure —
|
|
46
|
+
* excludes preamble locals and callback params) — present inside loop
|
|
47
|
+
* bodies. Named after, and fed from, `BindingScope.valueBoundNames()`
|
|
48
|
+
* (#2482 Stage 4 rename from `loopParams`, which predated the shared
|
|
49
|
+
* `BindingScope` service and named the mechanism, not the query). This
|
|
50
|
+
* is the REACTIVITY / SLOT-ID CLASSIFIER consumer class — see that
|
|
51
|
+
* method's docstring for why it must stay `valueBoundNames`, not
|
|
52
|
+
* `boundNames`.
|
|
53
|
+
*/
|
|
54
|
+
loopValueBoundNames?: ReadonlySet<string>
|
|
46
55
|
checker: ts.TypeChecker | null
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
|
50
59
|
* Per-environment cache for the binding map. `BindingEnvironment` identity
|
|
51
|
-
* is stable per (analyzer,
|
|
60
|
+
* is stable per (analyzer, loopValueBoundNames snapshot) — `jsx-to-ir.ts` memoizes
|
|
52
61
|
* `makeBindingEnv` so the same object is reused across every
|
|
53
62
|
* `resolveFreeRefs` call within a loop scope. With N expressions per
|
|
54
63
|
* component and M bindings per env, this drops binding-table construction
|
|
@@ -114,8 +123,8 @@ function buildBindingMap(env: BindingEnvironment): Map<string, BindingKind> {
|
|
|
114
123
|
map.set(m.name, 'memo-getter')
|
|
115
124
|
}
|
|
116
125
|
// Highest precedence — innermost scope.
|
|
117
|
-
if (env.
|
|
118
|
-
for (const name of env.
|
|
126
|
+
if (env.loopValueBoundNames) {
|
|
127
|
+
for (const name of env.loopValueBoundNames) map.set(name, 'render-item')
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
_bindingMapCache.set(env, map)
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,9 @@ export type {
|
|
|
57
57
|
TypeDefinition,
|
|
58
58
|
SourceLocation,
|
|
59
59
|
CompilerError,
|
|
60
|
+
ErrorSuggestion,
|
|
61
|
+
EscapeKind,
|
|
62
|
+
EscapeSsrCost,
|
|
60
63
|
ConformancePin,
|
|
61
64
|
ConformancePins,
|
|
62
65
|
RenderDivergences,
|
|
@@ -186,6 +189,9 @@ export interface BarefootPaths {
|
|
|
186
189
|
// AttrValue constructors
|
|
187
190
|
export { AttrValueOf } from './types.ts'
|
|
188
191
|
|
|
192
|
+
// Per-escape-kind SSR cost — the one place every renderer reads the trade from (#2613)
|
|
193
|
+
export { ESCAPE_SSR_COST } from './types.ts'
|
|
194
|
+
|
|
189
195
|
// CSS Layer Prefixer
|
|
190
196
|
export { applyCssLayerPrefix } from './css-layer-prefixer.ts'
|
|
191
197
|
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* marker elision"): decide, EXACTLY ONCE and BEFORE either `adapter.generate`
|
|
4
4
|
* (SSR) or `generateClientJs` (CSR) run, which `/* @client *\/` text slots
|
|
5
5
|
* can drop their `<!--bf:sN-->…<!--/-->` marker pair entirely from both
|
|
6
|
-
* outputs. Every consumer
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* outputs. Every consumer reads the single `IRExpression.markerless` flag
|
|
7
|
+
* this pass writes; nobody re-derives the decision, per CLAUDE.md's "Never
|
|
8
|
+
* add compiler options/hooks for tool-specific output rewriting" spirit:
|
|
9
|
+
* one door in, everyone reads it.
|
|
10
|
+
*
|
|
11
|
+
* The consumers are all nine SSR adapters' `renderExpression`, plus BOTH
|
|
12
|
+
* top-level CSR emitters in `html-template.ts` — `irToHtmlTemplate`'s
|
|
13
|
+
* `case 'expression'` and `generateCsrTemplateWithOpts`'s own. The second
|
|
14
|
+
* of those was wired up only by #2617; the flag shipped with just the
|
|
15
|
+
* first, which is exactly how the divergence that issue fixed went
|
|
16
|
+
* unnoticed.
|
|
11
17
|
*
|
|
12
18
|
* Scope — deliberately the NARROWEST slice of §3(b)'s elision rule that is
|
|
13
19
|
* fully sound today, not the general case:
|
|
@@ -5,10 +5,11 @@
|
|
|
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, buildLoopRowScope } 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'
|
|
14
15
|
import { identifierPattern } from '../identifier-pattern.ts'
|
|
@@ -1428,9 +1429,22 @@ export function collectLoopChildConditionals(
|
|
|
1428
1429
|
// paying for constant expansion — matches the legacy short-circuit.
|
|
1429
1430
|
if (!n.reactive && !refsLoopParamInSource) return
|
|
1430
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)
|
|
1431
1445
|
// Loop-param conditionals are reactive via per-item signal accessors;
|
|
1432
1446
|
// classifyReactivity sees both paths (signal/memo/prop + loop-param).
|
|
1433
|
-
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
|
|
1434
1448
|
|
|
1435
1449
|
const loopParamsForCond = loopParam
|
|
1436
1450
|
? [{ param: loopParam, bindings: loopParamBindings }]
|
|
@@ -1449,6 +1463,7 @@ export function collectLoopChildConditionals(
|
|
|
1449
1463
|
whenTrue: summarizeLoopChildBranch(n.whenTrue, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1450
1464
|
whenFalse: summarizeLoopChildBranch(n.whenFalse, ctx, siblingOffsets, loopParam, loopParamBindings, preambleNames, loopIndex),
|
|
1451
1465
|
...(expanded.freeIds !== undefined && { conditionFreeIdentifiers: expanded.freeIds }),
|
|
1466
|
+
...(readsPreamble && { readsPreamble: true }),
|
|
1452
1467
|
})
|
|
1453
1468
|
},
|
|
1454
1469
|
})
|
|
@@ -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 {
|
|
@@ -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)
|
|
@@ -729,6 +729,11 @@ function escapeLeafTextExpressions(ir: IRNode): IRNode {
|
|
|
729
729
|
}
|
|
730
730
|
}
|
|
731
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`).
|
|
732
737
|
export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, loopDepth = 0, loopParams?: ReadonlyArray<string | LoopParamSpec>, branchSlotsVar?: string, inHoistedChildren = false): string {
|
|
733
738
|
const recurse = (n: IRNode): string => irToHtmlTemplate(n, restSpreadNames, loopDepth, loopParams, branchSlotsVar, inHoistedChildren)
|
|
734
739
|
const wrapExpr = (expr: string) => wrapExprWithLoopParams(expr, loopParams)
|
|
@@ -805,6 +810,36 @@ export function irToHtmlTemplate(node: IRNode, restSpreadNames?: Set<string>, lo
|
|
|
805
810
|
// no-slotId branch below already uses — `elidedPath` (not this
|
|
806
811
|
// string) is what the claim plan resolves against, so no marker
|
|
807
812
|
// anchor is needed here at all.
|
|
813
|
+
//
|
|
814
|
+
// This branch evaluates `node.expr` eagerly, while the sibling
|
|
815
|
+
// top-level emitter (`generateCsrTemplateWithOpts`) emits NOTHING
|
|
816
|
+
// for its `markerless` case. That difference is safe only because
|
|
817
|
+
// this branch is UNREACHABLE today, which rests on three facts —
|
|
818
|
+
// all three must hold, so check them before widening either one:
|
|
819
|
+
//
|
|
820
|
+
// 1. `markerless` is only ever set by `markElided`, which
|
|
821
|
+
// `client-only-elision.ts` calls exclusively under
|
|
822
|
+
// `child.clientOnly && child.slotId`. So today
|
|
823
|
+
// `markerless === true` IMPLIES `clientOnly && slotId` — it is
|
|
824
|
+
// never some other, eagerly-renderable kind of markerless slot.
|
|
825
|
+
// 2. That walk never descends into a loop or conditional: its
|
|
826
|
+
// `default:` case freezes the level and returns without
|
|
827
|
+
// recursing (only `element` recurses).
|
|
828
|
+
// 3. This function only ever runs ON loop bodies and conditional
|
|
829
|
+
// branches (every caller passes `l.children[0]`, a `renderLeaf`,
|
|
830
|
+
// or `whenTrue`/`whenFalse`), and by its own docstring "does not
|
|
831
|
+
// honour `clientOnly`".
|
|
832
|
+
//
|
|
833
|
+
// (2) + (3) mean no marked node reaches here; (1) means that if one
|
|
834
|
+
// ever did, eager evaluation would be WRONG — it would render a
|
|
835
|
+
// deferred `/* @client */` read at template time instead of leaving
|
|
836
|
+
// it to init's createEffect. So if #2483 widens elision to cover
|
|
837
|
+
// loop/conditional subtrees, this branch stops being dead code and
|
|
838
|
+
// must gain the same `clientOnly` deferral check the sibling emitter
|
|
839
|
+
// has; it cannot simply keep evaluating. The two checks were left
|
|
840
|
+
// separate rather than collapsed into a shared helper precisely
|
|
841
|
+
// because their `clientOnly` semantics differ — see that function's
|
|
842
|
+
// own comment (#2617).
|
|
808
843
|
if (node.markerless) {
|
|
809
844
|
const bare = wrapInterpolation(wrapExpr(node.expr))
|
|
810
845
|
return `\${${bare}}`
|
|
@@ -2358,6 +2393,22 @@ function generateCsrTemplateWithOpts(node: IRNode, opts: TemplateOptions): strin
|
|
|
2358
2393
|
// the SSR adapters' `renderExpression` byte-for-byte (byte parity):
|
|
2359
2394
|
// empty at CSR mount too, since the expression is evaluated only
|
|
2360
2395
|
// once init's createEffect runs, same as the SSR case.
|
|
2396
|
+
//
|
|
2397
|
+
// Slot unification Step B (`markerless`, decided once by
|
|
2398
|
+
// `client-only-elision.ts` before this CSR pass runs; mirrors the
|
|
2399
|
+
// nested `if (expr.markerless) return ''` shape all nine SSR
|
|
2400
|
+
// adapters' `renderExpression` use inside their own identical
|
|
2401
|
+
// `clientOnly && slotId` branch — see `spec/slot-unification.md`
|
|
2402
|
+
// §5a): when the marker pair itself was ALSO elided, drop it and
|
|
2403
|
+
// emit nothing. The claim plan resolves the elided slot via
|
|
2404
|
+
// `elidedPath` (a precomputed child-index path), not a marker
|
|
2405
|
+
// scan, so no anchor comment is needed here at all — matches SSR's
|
|
2406
|
+
// fully-empty output for this case byte-for-byte (#2617; this is
|
|
2407
|
+
// the second of the two top-level CSR emitters `irToHtmlTemplate`
|
|
2408
|
+
// and `generateCsrTemplateWithOpts` that must each consult
|
|
2409
|
+
// `markerless` — see that function's own check at this file's
|
|
2410
|
+
// `case 'expression'` for why the two aren't collapsed into one).
|
|
2411
|
+
if (node.markerless) return ''
|
|
2361
2412
|
return `<!--bf:${node.slotId}--><!--/-->`
|
|
2362
2413
|
}
|
|
2363
2414
|
{
|
|
@@ -671,8 +671,10 @@ export function collectLoopChildReactiveTexts(
|
|
|
671
671
|
* `loopIndex` (Copilot review on #2595): see `collectLoopChildReactiveTexts`.
|
|
672
672
|
*/
|
|
673
673
|
/** Does any name in `names` appear in `set`? Iterates rather than
|
|
674
|
-
* spreading — this runs per attribute (Copilot review).
|
|
675
|
-
|
|
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 {
|
|
676
678
|
for (const n of names) if (set.has(n)) return true
|
|
677
679
|
return false
|
|
678
680
|
}
|
|
@@ -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 {
|
|
@@ -698,6 +698,21 @@ export interface LoopParamSpec {
|
|
|
698
698
|
* avoiding post-hoc regex replacement on full template strings.
|
|
699
699
|
*
|
|
700
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.
|
|
701
716
|
*/
|
|
702
717
|
export function wrapExprWithLoopParams(expr: string, loopParams?: ReadonlyArray<string | LoopParamSpec>): string {
|
|
703
718
|
if (!loopParams) return expr
|
package/src/jsx-to-ir.ts
CHANGED
|
@@ -845,7 +845,7 @@ function makeBindingEnv(ctx: TransformContext): BindingEnvironment {
|
|
|
845
845
|
// mutated (cached on the immutable `BindingScope`) — a stable
|
|
846
846
|
// snapshot even if `ctx.scope` is later reassigned by an enclosing
|
|
847
847
|
// visitor frame, which swaps the instance rather than mutating it.
|
|
848
|
-
|
|
848
|
+
loopValueBoundNames: boundNames,
|
|
849
849
|
checker: a.checker,
|
|
850
850
|
}
|
|
851
851
|
ctx._bindingEnv = env
|
|
@@ -5021,6 +5021,13 @@ function transformMapCall(
|
|
|
5021
5021
|
// the row effect re-runs the preamble ahead of the write.
|
|
5022
5022
|
if (preamble && !isStaticArray) {
|
|
5023
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
|
+
}
|
|
5024
5031
|
}
|
|
5025
5032
|
|
|
5026
5033
|
// Collect nested components for both static and dynamic arrays.
|
|
@@ -5457,6 +5464,67 @@ function markPreambleAttrSlots(
|
|
|
5457
5464
|
visit(nodes)
|
|
5458
5465
|
}
|
|
5459
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
|
+
|
|
5460
5528
|
/**
|
|
5461
5529
|
* Expression-bearing source text of an attribute value, for the
|
|
5462
5530
|
* free-identifier scan. Only used as a FALLBACK — an attribute normally
|
|
@@ -5503,6 +5571,52 @@ function collectPreambleDeclaredNames(stmt: ts.Statement, out: Set<string>): voi
|
|
|
5503
5571
|
}
|
|
5504
5572
|
}
|
|
5505
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
|
+
|
|
5506
5620
|
/**
|
|
5507
5621
|
* Build a {@link MapCallbackPreamble} from value-only (JSX-free) pre-return
|
|
5508
5622
|
* statements — the Stage-2 fold preamble and the plain block-body preamble.
|
|
@@ -5526,6 +5640,7 @@ function preambleFromValueStatements(
|
|
|
5526
5640
|
typedParts.push(raw0.endsWith(';') ? raw0 : raw0 + ';')
|
|
5527
5641
|
segments.push(tjs !== js ? { kind: 'js', text: js, templateText: tjs } : { kind: 'js', text: js })
|
|
5528
5642
|
}
|
|
5643
|
+
const reactiveNames = computePreambleReactiveNames(statements, ctx)
|
|
5529
5644
|
return {
|
|
5530
5645
|
segments: trimPreambleSegments(segments),
|
|
5531
5646
|
ssrText: tsxSourceText(typedParts.join(' ')),
|
|
@@ -5533,6 +5648,7 @@ function preambleFromValueStatements(
|
|
|
5533
5648
|
// Value-only preambles accumulate no JSX, so no child needs the array join.
|
|
5534
5649
|
builderNames: [],
|
|
5535
5650
|
declarations: neutralPreambleDeclarations(statements, ctx) ?? undefined,
|
|
5651
|
+
reactiveNames: reactiveNames.size > 0 ? [...reactiveNames] : undefined,
|
|
5536
5652
|
}
|
|
5537
5653
|
}
|
|
5538
5654
|
|