@barefootjs/jsx 0.31.1 → 0.31.3
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/interface.d.ts +11 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +92 -1
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/css-layer-prefixer.d.ts +16 -0
- package/dist/css-layer-prefixer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/html-types.d.ts +19 -0
- package/dist/html-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1531 -1155
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-event-handlers.d.ts.map +1 -1
- package/dist/ir-to-client-js/phases/props-extraction.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +6 -4
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-runtime/index.d.ts +2 -8
- package/dist/jsx-runtime/index.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/module-exports.d.ts +9 -1
- package/dist/module-exports.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +40 -0
- package/dist/props-binding.d.ts.map +1 -0
- package/dist/relocate.d.ts +9 -0
- package/dist/relocate.d.ts.map +1 -1
- package/dist/scope/binding-scope.d.ts +179 -0
- package/dist/scope/binding-scope.d.ts.map +1 -0
- package/dist/ssr-defaults.d.ts +43 -0
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/template-parts.d.ts +53 -0
- package/dist/template-parts.d.ts.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapter-output.test.ts +8 -4
- package/src/__tests__/aliased-destructured-prop-csr.test.ts +112 -0
- package/src/__tests__/binding-scope-preamble-shadowing.test.ts +115 -0
- package/src/__tests__/binding-scope-ratchet.test.ts +194 -0
- package/src/__tests__/binding-scope.test.ts +200 -0
- package/src/__tests__/css-layer-prefixer.test.ts +72 -0
- package/src/__tests__/form-control-value-ssr.test.ts +48 -3
- package/src/__tests__/let-type-annotation.test.ts +208 -0
- package/src/__tests__/memo-deps-comments.test.ts +99 -0
- package/src/__tests__/multi-return-sibling-diagnostic.test.ts +241 -0
- package/src/__tests__/ssr-defaults.test.ts +124 -1
- package/src/__tests__/staged-ir/08-relocate-unit.test.ts +1 -0
- package/src/__tests__/staged-ir/11-template-primitive-registry.test.ts +1 -0
- package/src/adapters/interface.ts +11 -0
- package/src/adapters/jsx-adapter.ts +295 -5
- package/src/adapters/test-adapter.ts +13 -10
- package/src/analyzer.ts +70 -11
- package/src/compiler.ts +119 -18
- package/src/css-layer-prefixer.ts +80 -24
- package/src/errors.ts +18 -0
- package/src/html-types.ts +24 -0
- package/src/index.ts +11 -1
- package/src/ir-to-client-js/collect-elements.ts +4 -1
- package/src/ir-to-client-js/emit-reactive.ts +4 -2
- package/src/ir-to-client-js/phases/props-event-handlers.ts +4 -3
- package/src/ir-to-client-js/phases/props-extraction.ts +7 -4
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +6 -3
- package/src/ir-to-client-js/utils.ts +5 -24
- package/src/jsx-runtime/index.ts +2 -7
- package/src/jsx-to-ir.ts +245 -107
- package/src/module-exports.ts +11 -2
- package/src/prop-rewrite.ts +26 -6
- package/src/props-binding.ts +70 -0
- package/src/relocate.ts +19 -2
- package/src/scope/binding-scope.ts +238 -0
- package/src/ssr-defaults.ts +70 -0
- package/src/template-parts.ts +81 -0
- package/src/types.ts +18 -0
package/src/jsx-to-ir.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
rewriteBarePropRefs as rewriteBarePropRefsCore,
|
|
50
50
|
collectAstPropRefs,
|
|
51
51
|
} from './prop-rewrite.ts'
|
|
52
|
+
import { buildPropAliasMap } from './props-binding.ts'
|
|
52
53
|
import { resolveFreeRefs, isNameBound as isNameBoundInEnv, type BindingEnvironment } from './free-refs.ts'
|
|
53
54
|
import { computeFileScope } from './ir-to-client-js/component-scope.ts'
|
|
54
55
|
import { createTemplateAwareStringProtector } from './ir-to-client-js/html-template.ts'
|
|
@@ -59,7 +60,9 @@ import type { LoweringMatcher } from './lowering-registry.ts'
|
|
|
59
60
|
import { extractFreeIdentifiersFromNode, initializerShapeContainsJsx, extractMultiReturnJsxBranches, type MultiReturnJsxBranches } from './analyzer.ts'
|
|
60
61
|
import { iterateJsTokens, replaceInExprContexts } from './scanner/js-scanner.ts'
|
|
61
62
|
import { reconstructAsSegments } from './strip-types.ts'
|
|
63
|
+
import { templatePartsToJsExpr } from './template-parts.ts'
|
|
62
64
|
import { toHTMLAttrName, decodeEntities } from '@barefootjs/shared'
|
|
65
|
+
import { BindingScope } from './scope/binding-scope.ts'
|
|
63
66
|
|
|
64
67
|
// =============================================================================
|
|
65
68
|
// Transform Context
|
|
@@ -91,12 +94,27 @@ interface TransformContext {
|
|
|
91
94
|
_moduleClientSignalNames?: Set<string>
|
|
92
95
|
/** Cached set of destructured prop names for AST-based rewriting */
|
|
93
96
|
_destructuredPropNames?: Set<string> | null
|
|
94
|
-
/**
|
|
95
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Cached local-name → caller-facing-key (`sourceName ?? name`) map for
|
|
99
|
+
* `_destructuredPropNames`, entries only for names that actually rename
|
|
100
|
+
* (`{ n: count }` → `count` → `n`). Built alongside `_destructuredPropNames`
|
|
101
|
+
* so both stay in lockstep with the same shadow-filtered eligible set —
|
|
102
|
+
* consumed by `rewriteBarePropRefsCore` to emit `_p.<caller>` instead of
|
|
103
|
+
* `_p.<local>` (#2524 CSR half: `_p` is always caller-keyed).
|
|
104
|
+
*/
|
|
105
|
+
_destructuredPropAliases?: Map<string, string> | null
|
|
106
|
+
/**
|
|
107
|
+
* The active `.map()`/callback binding stack (#2482 Stage 1a) — replaces
|
|
108
|
+
* the former mutable per-name `Set<string>` mutated in lockstep with
|
|
109
|
+
* `transformMapCall` entry/exit. `enterLoopRow`/`enterCallback` return a
|
|
110
|
+
* NEW `BindingScope`; restoring the saved parent reference on exit is
|
|
111
|
+
* the whole mechanism (no `.delete()` bookkeeping to get wrong).
|
|
112
|
+
*/
|
|
113
|
+
scope: BindingScope
|
|
96
114
|
/**
|
|
97
115
|
* Count of enclosing `.map()` loops (0 = outermost), incremented/
|
|
98
116
|
* decremented in lockstep with entering/leaving `transformMapCall`.
|
|
99
|
-
* Unlike `
|
|
117
|
+
* Unlike `scope` (a binding stack that can gain several bound names for
|
|
100
118
|
* ONE loop level via destructuring), this is a plain per-level
|
|
101
119
|
* counter — the single source of truth `IRLoop.depth` is stamped
|
|
102
120
|
* from, so every adapter's `data-key`/`data-key-N` suffix derives
|
|
@@ -122,8 +140,9 @@ interface TransformContext {
|
|
|
122
140
|
/**
|
|
123
141
|
* Memoized free-refs binding environment. Built lazily by
|
|
124
142
|
* `makeBindingEnv` and reused across every `resolveFreeRefs` call as
|
|
125
|
-
* long as `
|
|
126
|
-
* `
|
|
143
|
+
* long as `scope`'s bound VALUE names are unchanged. Invalidated by
|
|
144
|
+
* serializing `scope.valueBoundNames()` into `_bindingEnvLoopKey` and
|
|
145
|
+
* comparing on read.
|
|
127
146
|
*/
|
|
128
147
|
_bindingEnv?: BindingEnvironment
|
|
129
148
|
_bindingEnvLoopKey?: string
|
|
@@ -514,15 +533,21 @@ function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext)
|
|
|
514
533
|
let propNames = getDestructuredPropNames(ctx)
|
|
515
534
|
if (!propNames) return dateLowered === text ? undefined : dateLowered
|
|
516
535
|
// #2222: a name bound as an enclosing loop callback's item/index param
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
536
|
+
// (or, during the return-expression transform window, a preamble-
|
|
537
|
+
// declared local — #2482 Stage 1a Commit 2 re-enters `ctx.scope` with
|
|
538
|
+
// the preamble for that window) refers to the loop binding, not the
|
|
539
|
+
// prop, at THIS transform position — maintained by `transformMapCall`
|
|
540
|
+
// as it enters/leaves each callback, so this guard is scope-accurate
|
|
541
|
+
// rather than the coarse whole-component exclusion the SSR adapters use
|
|
542
|
+
// (#2221). This is a SHADOW-GUARD query — every `ScopeBindingSource`
|
|
543
|
+
// qualifies, so it reads all-sources `boundNames()`, not
|
|
544
|
+
// `valueBoundNames()` (see that method's doc comment on
|
|
545
|
+
// `BindingScope` for the two-consumer-classes split). Filter into a
|
|
546
|
+
// fresh set — `getDestructuredPropNames` caches its set on ctx and must
|
|
547
|
+
// not be mutated.
|
|
548
|
+
const shadowingNames = ctx.scope.boundNames()
|
|
549
|
+
if (shadowingNames.size > 0) {
|
|
550
|
+
const filtered = new Set([...propNames].filter(n => !shadowingNames.has(n)))
|
|
526
551
|
if (filtered.size === 0) return dateLowered === text ? undefined : dateLowered
|
|
527
552
|
propNames = filtered
|
|
528
553
|
}
|
|
@@ -536,7 +561,8 @@ function rewriteBarePropRefs(text: string, expr: ts.Node, ctx: TransformContext)
|
|
|
536
561
|
// `_branchScopePropDeps` at branch entry; here we just walk `expr`
|
|
537
562
|
// for references to those locals and union the matching dep sets.
|
|
538
563
|
const extraPropRefs = collectBranchLocalPropRefsViaSubstitution(expr, ctx)
|
|
539
|
-
|
|
564
|
+
const propAliases = getDestructuredPropAliases(ctx)
|
|
565
|
+
return rewriteBarePropRefsCore(dateLowered, expr, propNames, extraPropRefs, propAliases ?? undefined)
|
|
540
566
|
}
|
|
541
567
|
|
|
542
568
|
/**
|
|
@@ -618,14 +644,26 @@ function getDestructuredPropNames(ctx: TransformContext): Set<string> | null {
|
|
|
618
644
|
if (!isDestructureFromProps) shadowed.add(c.name)
|
|
619
645
|
}
|
|
620
646
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
.filter(n => !shadowed.has(n))
|
|
647
|
+
const eligible = ctx.analyzer.propsParams.filter(p => !shadowed.has(p.name))
|
|
648
|
+
const names = eligible.map(p => p.name)
|
|
624
649
|
ctx._destructuredPropNames = names.length > 0 ? new Set(names) : null
|
|
650
|
+
ctx._destructuredPropAliases = buildPropAliasMap(eligible) ?? null
|
|
625
651
|
}
|
|
626
652
|
return ctx._destructuredPropNames ?? null
|
|
627
653
|
}
|
|
628
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Companion to `getDestructuredPropNames`: the local-name → caller-key
|
|
657
|
+
* alias map for the SAME shadow-filtered eligible set, populated as a
|
|
658
|
+
* side effect of that call. Always call `getDestructuredPropNames` first
|
|
659
|
+
* (or accept it may return an empty cache) — the two caches are written
|
|
660
|
+
* together in one pass.
|
|
661
|
+
*/
|
|
662
|
+
function getDestructuredPropAliases(ctx: TransformContext): Map<string, string> | null {
|
|
663
|
+
if (ctx._destructuredPropNames === undefined) getDestructuredPropNames(ctx)
|
|
664
|
+
return ctx._destructuredPropAliases ?? null
|
|
665
|
+
}
|
|
666
|
+
|
|
629
667
|
function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
630
668
|
return {
|
|
631
669
|
analyzer,
|
|
@@ -638,7 +676,7 @@ function createTransformContext(analyzer: AnalyzerContext): TransformContext {
|
|
|
638
676
|
spreadIdCounter: 0,
|
|
639
677
|
isRoot: true,
|
|
640
678
|
insideComponentChildren: false,
|
|
641
|
-
|
|
679
|
+
scope: BindingScope.EMPTY,
|
|
642
680
|
loopDepth: 0,
|
|
643
681
|
patterns: {
|
|
644
682
|
signals: analyzer.signals.map(s => ({
|
|
@@ -767,21 +805,27 @@ function generateSpreadSlotId(ctx: TransformContext): string {
|
|
|
767
805
|
/**
|
|
768
806
|
* Build the binding environment for `resolveFreeRefs` from the current
|
|
769
807
|
* transform context. The analyzer's collected bindings (signals, memos,
|
|
770
|
-
* props, locals, imports) plus the active loop
|
|
771
|
-
*
|
|
772
|
-
*
|
|
808
|
+
* props, locals, imports) plus the active loop-bound VALUE names (item/
|
|
809
|
+
* index/destructure — see `BindingScope.valueBoundNames`'s doc comment
|
|
810
|
+
* for why preamble locals are excluded here, #2482 Stage 1a Commit 2)
|
|
811
|
+
* form the resolution frame; the TypeChecker, when available, is
|
|
812
|
+
* forwarded so library getters carrying the Reactive<T> brand are
|
|
813
|
+
* recognised.
|
|
773
814
|
*
|
|
774
815
|
* Memoized on `ctx`: the returned env is identity-stable as long as
|
|
775
|
-
* `
|
|
776
|
-
* binding-table cache in `free-refs.ts` warm across every
|
|
777
|
-
* the same loop scope. `
|
|
778
|
-
* visitor enters / leaves `.map()` callbacks
|
|
779
|
-
*
|
|
816
|
+
* `ctx.scope`'s bound VALUE names are unchanged, which keeps the
|
|
817
|
+
* `WeakMap`-keyed binding-table cache in `free-refs.ts` warm across every
|
|
818
|
+
* expression in the same loop scope. `ctx.scope` is REASSIGNED (never
|
|
819
|
+
* mutated) as the visitor enters / leaves `.map()` callbacks (#2482 Stage
|
|
820
|
+
* 1a), so we serialize its bound names into a key rather than relying on
|
|
821
|
+
* object identity — a sibling loop at the same nesting level would
|
|
822
|
+
* otherwise get a spurious cache miss/hit mismatch across restores.
|
|
780
823
|
*/
|
|
781
824
|
function makeBindingEnv(ctx: TransformContext): BindingEnvironment {
|
|
782
|
-
const
|
|
825
|
+
const boundNames = ctx.scope.valueBoundNames()
|
|
826
|
+
const loopKey = boundNames.size === 0
|
|
783
827
|
? ''
|
|
784
|
-
: Array.from(
|
|
828
|
+
: Array.from(boundNames).sort().join('\0')
|
|
785
829
|
if (ctx._bindingEnv && ctx._bindingEnvLoopKey === loopKey) {
|
|
786
830
|
return ctx._bindingEnv
|
|
787
831
|
}
|
|
@@ -796,9 +840,11 @@ function makeBindingEnv(ctx: TransformContext): BindingEnvironment {
|
|
|
796
840
|
localFunctions: a.localFunctions,
|
|
797
841
|
imports: a.imports,
|
|
798
842
|
ambientGlobals: a.ambientGlobals,
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
843
|
+
// `valueBoundNames()` returns a per-instance set that is never
|
|
844
|
+
// mutated (cached on the immutable `BindingScope`) — a stable
|
|
845
|
+
// snapshot even if `ctx.scope` is later reassigned by an enclosing
|
|
846
|
+
// visitor frame, which swaps the instance rather than mutating it.
|
|
847
|
+
loopParams: boundNames,
|
|
802
848
|
checker: a.checker,
|
|
803
849
|
}
|
|
804
850
|
ctx._bindingEnv = env
|
|
@@ -1278,12 +1324,21 @@ function transformJsxElement(
|
|
|
1278
1324
|
* - `<textarea>` with no children gains a NON-reactive expression child
|
|
1279
1325
|
* (the initial value as element content — updates keep flowing through
|
|
1280
1326
|
* the `.value` effect, deliberately not a live text slot);
|
|
1281
|
-
* - `<select>` distributes `selected={(value) ===
|
|
1282
|
-
*
|
|
1283
|
-
* the exact per-option comparison shape the
|
|
1284
|
-
* fixture already proves across every adapter.
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1327
|
+
* - `<select>` distributes `selected={(value) === optValue}` onto each
|
|
1328
|
+
* `<option>` (incl. under `<optgroup>`/fragments, and under a `.map()`
|
|
1329
|
+
* loop body) — the exact per-option comparison shape the
|
|
1330
|
+
* `select-option-selected` fixture already proves across every adapter.
|
|
1331
|
+
* A literal `optValue` (e.g. `value="banana"`) compares by
|
|
1332
|
+
* `JSON.stringify`; an expression `optValue` (a static dynamic value, or
|
|
1333
|
+
* a loop row reading its item — e.g. `value={o.id}`) compares against
|
|
1334
|
+
* the expression text directly. For a loop row this makes `selected` an
|
|
1335
|
+
* ordinary per-item reactive attribute like any other (`o.id`, the row's
|
|
1336
|
+
* text) — it rides the SAME loop-plan machinery
|
|
1337
|
+
* (`collectLoopChildReactiveAttrs` → `emitAttrUpdate`, which special-cases
|
|
1338
|
+
* `selected` as a boolean DOM PROPERTY write, not just an HTML attribute)
|
|
1339
|
+
* that already reruns per row on item change and per outer-signal change,
|
|
1340
|
+
* so selectedness is recomputed instead of staying attached to whichever
|
|
1341
|
+
* physical `<option>` a reorder happened to rewrite in place (#2466).
|
|
1287
1342
|
*/
|
|
1288
1343
|
function lowerFormControlValueSsr(
|
|
1289
1344
|
tagName: string,
|
|
@@ -1318,21 +1373,47 @@ function lowerFormControlValueSsr(
|
|
|
1318
1373
|
return
|
|
1319
1374
|
}
|
|
1320
1375
|
|
|
1321
|
-
const
|
|
1376
|
+
const selectedForLiteral = (optValue: string): AttrValue =>
|
|
1322
1377
|
AttrValueOf.expression(
|
|
1323
1378
|
`(${expr}) === ${JSON.stringify(optValue)}`,
|
|
1324
1379
|
templateExpr !== undefined
|
|
1325
1380
|
? { templateExpr: `(${templateExpr}) === ${JSON.stringify(optValue)}` }
|
|
1326
1381
|
: undefined,
|
|
1327
1382
|
)
|
|
1383
|
+
// An expression-valued `option value` — the shape every `.map()` loop row
|
|
1384
|
+
// uses (`value={o.id}`) since the whole point of the loop is a per-item
|
|
1385
|
+
// value. Compares the controlled value directly against the option's own
|
|
1386
|
+
// value EXPRESSION TEXT (never JSON-stringified — it is JS, not a string
|
|
1387
|
+
// literal). Inside a loop row this expression reads both the row item
|
|
1388
|
+
// (`o.id`) and the outer controlled signal (`expr`, e.g. `val()`), which
|
|
1389
|
+
// is exactly the "reads both" shape `collectLoopChildReactiveAttrs` /
|
|
1390
|
+
// `classifyLazyBinding` already know how to place into both `applyItem`
|
|
1391
|
+
// (row changed) and `applyOuter` (controlled value changed) — no new loop
|
|
1392
|
+
// machinery, just one more per-row reactive attribute (#2466).
|
|
1393
|
+
const selectedForExpr = (optExpr: string, optTemplateExpr: string | undefined): AttrValue =>
|
|
1394
|
+
AttrValueOf.expression(
|
|
1395
|
+
`(${expr}) === (${optExpr})`,
|
|
1396
|
+
templateExpr !== undefined || optTemplateExpr !== undefined
|
|
1397
|
+
? { templateExpr: `(${templateExpr ?? expr}) === (${optTemplateExpr ?? optExpr})` }
|
|
1398
|
+
: undefined,
|
|
1399
|
+
)
|
|
1328
1400
|
const distribute = (nodes: IRNode[]): void => {
|
|
1329
1401
|
for (const n of nodes) {
|
|
1330
1402
|
if (n.type === 'element' && n.tag === 'option') {
|
|
1331
1403
|
if (n.attrs.some(a => a.name === 'selected')) continue
|
|
1332
1404
|
const optValue = n.attrs.find(a => a.name === 'value')
|
|
1333
|
-
if (!optValue
|
|
1334
|
-
|
|
1335
|
-
|
|
1405
|
+
if (!optValue) continue
|
|
1406
|
+
if (optValue.value.kind === 'literal') {
|
|
1407
|
+
n.attrs.push({ name: 'selected', value: selectedForLiteral(optValue.value.value), loc: n.loc })
|
|
1408
|
+
} else if (optValue.value.kind === 'expression') {
|
|
1409
|
+
const selected = selectedForExpr(optValue.value.expr, optValue.value.templateExpr)
|
|
1410
|
+
n.attrs.push({ name: 'selected', value: selected, loc: n.loc })
|
|
1411
|
+
}
|
|
1412
|
+
} else if (
|
|
1413
|
+
n.type === 'fragment' ||
|
|
1414
|
+
n.type === 'loop' ||
|
|
1415
|
+
(n.type === 'element' && n.tag === 'optgroup')
|
|
1416
|
+
) {
|
|
1336
1417
|
distribute(n.children)
|
|
1337
1418
|
}
|
|
1338
1419
|
}
|
|
@@ -2110,9 +2191,17 @@ function transformExpressionInner(
|
|
|
2110
2191
|
const reactive = isReactiveExpression(exprText, ctx, expr) || isReactiveOrigin(origin)
|
|
2111
2192
|
// @client expressions always need slotId and are treated as reactive for client-side evaluation
|
|
2112
2193
|
// Expressions inside loops that reference the loop parameter need slotId
|
|
2113
|
-
// so fine-grained effects can target them for per-item signal updates
|
|
2114
|
-
|
|
2115
|
-
|
|
2194
|
+
// so fine-grained effects can target them for per-item signal updates.
|
|
2195
|
+
// REACTIVITY/slotId classifier — value bindings only (item/index/
|
|
2196
|
+
// destructure), NOT preamble locals: those already get their own
|
|
2197
|
+
// dedicated slot/region-patch machinery (#2447), so folding them in
|
|
2198
|
+
// here would double-allocate and (via `hasDynamicContent` reading this
|
|
2199
|
+
// same `reactive`-adjacent signal) move an unrelated row-root slotId
|
|
2200
|
+
// decision — see `BindingScope.valueBoundNames`'s doc comment
|
|
2201
|
+
// (#2482 Stage 1a Commit 2).
|
|
2202
|
+
const scopeValueNames = ctx.scope.valueBoundNames()
|
|
2203
|
+
const refsLoopParam = scopeValueNames.size > 0
|
|
2204
|
+
&& Array.from(scopeValueNames).some(p => new RegExp(`\\b${p}\\b`).test(exprText))
|
|
2116
2205
|
|
|
2117
2206
|
// Compute AST-derived flags. `callsReactive` recognises signal-getter / memo
|
|
2118
2207
|
// calls even inside deeper expressions (e.g., `format(count())`); `hasCalls`
|
|
@@ -3962,8 +4051,13 @@ function transformMapCall(
|
|
|
3962
4051
|
method: 'map' | 'flatMap' = 'map'
|
|
3963
4052
|
): IRLoop | null {
|
|
3964
4053
|
// Capture nesting depth before we register this map's own params.
|
|
3965
|
-
// ctx.
|
|
3966
|
-
|
|
4054
|
+
// ctx.scope is populated by the *outer* map; if any VALUE names (item/
|
|
4055
|
+
// index/destructure) are bound we are inside one. Reads
|
|
4056
|
+
// `valueBoundNames()`, not `boundNames()`, for consistency with the
|
|
4057
|
+
// other structural/reactivity consumers (#2482 Stage 1a Commit 2) —
|
|
4058
|
+
// though a bound outer-loop row always carries at least one value
|
|
4059
|
+
// binding regardless, so this can't actually change the answer.
|
|
4060
|
+
const isNested = ctx.scope.valueBoundNames().size > 0
|
|
3967
4061
|
// Diagnostic count at entry — the structural net at the scalar fallthrough
|
|
3968
4062
|
// de-dups against refusals fired DURING this call (leaf-wiring, DSL gates),
|
|
3969
4063
|
// never against unrelated diagnostics recorded before it.
|
|
@@ -4263,17 +4357,17 @@ function transformMapCall(
|
|
|
4263
4357
|
}
|
|
4264
4358
|
}
|
|
4265
4359
|
|
|
4266
|
-
// Register loop
|
|
4267
|
-
//
|
|
4268
|
-
//
|
|
4269
|
-
// `
|
|
4270
|
-
//
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4360
|
+
// Register loop-bound names so expressions referencing them get slotId,
|
|
4361
|
+
// by entering a new BindingScope frame for this row (#2482 Stage 1a).
|
|
4362
|
+
// For destructured patterns, the frame binds the individual binding
|
|
4363
|
+
// names — `\b${param}\b` never matches a bare name like `cfg` when
|
|
4364
|
+
// `param` is `[, cfg]`, which would otherwise leave reactive-expression
|
|
4365
|
+
// detection silently broken for destructured callbacks.
|
|
4366
|
+
// `savedScope` is restored (not deleted-from) at the matching exit site
|
|
4367
|
+
// below, so a nested loop reusing a param name can never corrupt the
|
|
4368
|
+
// outer scope.
|
|
4369
|
+
const savedScope = ctx.scope
|
|
4370
|
+
ctx.scope = ctx.scope.enterLoopRow({ param, index, paramBindings })
|
|
4277
4371
|
ctx.loopDepth++
|
|
4278
4372
|
|
|
4279
4373
|
// Logical control flow (`cond && <X/>`, `a ?? themeLogo()`) as the map
|
|
@@ -4400,6 +4494,51 @@ function transformMapCall(
|
|
|
4400
4494
|
(s): s is ts.ReturnStatement => ts.isReturnStatement(s) && s.expression != null
|
|
4401
4495
|
)
|
|
4402
4496
|
: undefined
|
|
4497
|
+
|
|
4498
|
+
// #2482 Stage 1a Commit 2 — ordering note: the STRUCTURED preamble
|
|
4499
|
+
// (`MapCallbackPreamble`, built further down by
|
|
4500
|
+
// `preambleFromValueStatements` / `buildPreambleSegments`, whichever
|
|
4501
|
+
// branch below the return-expression shape takes) isn't known until
|
|
4502
|
+
// well AFTER this point. But `transformNode(returnExpr, ctx)` just
|
|
4503
|
+
// below walks the return expression's own child expressions RIGHT
|
|
4504
|
+
// NOW — before that happens — so a preamble-declared local shadowing
|
|
4505
|
+
// an outer const/prop must already be bound in `ctx.scope`, or
|
|
4506
|
+
// `tryResolveTemplateSpanFromConst` / `tryResolveIdentifierAsTemplateLiteral`
|
|
4507
|
+
// / `rewriteBarePropRefs` bake the OUTER value into every row instead
|
|
4508
|
+
// of leaving the row-local unresolved (#2222-family, the const/prop-
|
|
4509
|
+
// shadow bug class). Fix: a lightweight declared-names-only pre-scan
|
|
4510
|
+
// — the exact same "statements before the return" shape
|
|
4511
|
+
// `preambleFromValueStatements`/`buildPreambleSegments` scan below —
|
|
4512
|
+
// re-enters the row's `BindingScope` frame (off `savedScope`, the
|
|
4513
|
+
// pre-loop parent, so this REPLACES the param/index-only frame
|
|
4514
|
+
// rather than stacking a second one) with the preamble included, for
|
|
4515
|
+
// the duration of the return-expression transform. Restored back to
|
|
4516
|
+
// the preamble-less row scope right after that window closes (see
|
|
4517
|
+
// `rowScopeBeforePreamble` below) — everything past this window
|
|
4518
|
+
// (the flatMap fallbacks, and the whole rest of `transformMapCall`
|
|
4519
|
+
// after this `if (ts.isBlock(body))` branch) must keep observing
|
|
4520
|
+
// EXACTLY the old (Commit 1) membership, since slotId-allocation
|
|
4521
|
+
// classifiers there read `ctx.scope.valueBoundNames()`, which never
|
|
4522
|
+
// included preamble names to begin with — only shadow guards need
|
|
4523
|
+
// the widened view, and only for this window.
|
|
4524
|
+
let rowScopeBeforePreamble: BindingScope | null = null
|
|
4525
|
+
if (returnStmt) {
|
|
4526
|
+
const preambleNames = new Set<string>()
|
|
4527
|
+
for (const stmt of body.statements) {
|
|
4528
|
+
if (stmt === returnStmt) break
|
|
4529
|
+
collectPreambleDeclaredNames(stmt, preambleNames)
|
|
4530
|
+
}
|
|
4531
|
+
if (preambleNames.size > 0) {
|
|
4532
|
+
rowScopeBeforePreamble = ctx.scope
|
|
4533
|
+
ctx.scope = savedScope.enterLoopRow({
|
|
4534
|
+
param,
|
|
4535
|
+
index,
|
|
4536
|
+
paramBindings,
|
|
4537
|
+
preamble: { declaredNames: [...preambleNames] },
|
|
4538
|
+
})
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4403
4542
|
if (returnStmt && returnStmt.expression) {
|
|
4404
4543
|
let returnExpr = returnStmt.expression
|
|
4405
4544
|
while (ts.isParenthesizedExpression(returnExpr)) {
|
|
@@ -4558,6 +4697,14 @@ function transformMapCall(
|
|
|
4558
4697
|
}
|
|
4559
4698
|
}
|
|
4560
4699
|
|
|
4700
|
+
// Window closed — restore the preamble-less row scope so every
|
|
4701
|
+
// consumer past this point (the flatMap fallbacks immediately
|
|
4702
|
+
// below, and the rest of `transformMapCall` after this branch)
|
|
4703
|
+
// keeps observing EXACTLY the pre-rework membership.
|
|
4704
|
+
if (rowScopeBeforePreamble) {
|
|
4705
|
+
ctx.scope = rowScopeBeforePreamble
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4561
4708
|
// flatMap block body fallback: compile JSX inline when children
|
|
4562
4709
|
// couldn't be extracted via the standard single-return path. A pure
|
|
4563
4710
|
// single-`return <call>` projection is NOT taken here — it lowers to
|
|
@@ -4638,13 +4785,10 @@ function transformMapCall(
|
|
|
4638
4785
|
)
|
|
4639
4786
|
}
|
|
4640
4787
|
|
|
4641
|
-
//
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
ctx.loopParams.delete(param)
|
|
4646
|
-
}
|
|
4647
|
-
if (index) ctx.loopParams.delete(index)
|
|
4788
|
+
// Restore the parent scope — the BindingScope twin of the old
|
|
4789
|
+
// "unregister loop params" delete block, but by reference rather than
|
|
4790
|
+
// by name, so it can never miss an entry the add-site added.
|
|
4791
|
+
ctx.scope = savedScope
|
|
4648
4792
|
ctx.loopDepth--
|
|
4649
4793
|
}
|
|
4650
4794
|
|
|
@@ -6075,10 +6219,14 @@ function tryResolveTemplateSpanFromConst(
|
|
|
6075
6219
|
// ${IDENT}
|
|
6076
6220
|
if (ts.isIdentifier(expr)) {
|
|
6077
6221
|
// #2222-family: inside a loop callback the name may be the loop's
|
|
6078
|
-
// item/index binding shadowing a same-named const —
|
|
6079
|
-
// const would bake the outer value into every row.
|
|
6080
|
-
// the bare-expression path, which sees the loop
|
|
6081
|
-
|
|
6222
|
+
// item/index/preamble binding shadowing a same-named const —
|
|
6223
|
+
// resolving the const would bake the outer value into every row.
|
|
6224
|
+
// Fall back to the bare-expression path, which sees the loop
|
|
6225
|
+
// binding. SHADOW-GUARD query — `isBound` (all sources), not
|
|
6226
|
+
// `valueBoundNames()` — see `BindingScope.valueBoundNames`'s doc
|
|
6227
|
+
// comment for the two-consumer-classes split (#2482 Stage 1a
|
|
6228
|
+
// Commit 2).
|
|
6229
|
+
if (ctx.scope.isBound(expr.text)) return null
|
|
6082
6230
|
const constInfo = findLocalConst(expr.text, ctx.analyzer)
|
|
6083
6231
|
if (!constInfo) return null
|
|
6084
6232
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -6095,7 +6243,7 @@ function tryResolveTemplateSpanFromConst(
|
|
|
6095
6243
|
// Same loop-shadowing guard as the ${IDENT} arm: `tone[k]` inside
|
|
6096
6244
|
// `items.map((tone) => …)` must read the row's `tone`, not a
|
|
6097
6245
|
// same-named module/component record const.
|
|
6098
|
-
if (ctx.
|
|
6246
|
+
if (ctx.scope.isBound(expr.expression.text)) return null
|
|
6099
6247
|
const constInfo = findLocalConst(expr.expression.text, ctx.analyzer)
|
|
6100
6248
|
if (!constInfo) return null
|
|
6101
6249
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -6267,9 +6415,13 @@ function tryResolveIdentifierAsTemplateLiteral(
|
|
|
6267
6415
|
// outer const's literal into the IR here bakes the same hard-coded
|
|
6268
6416
|
// value into EVERY adapter's output (e.g. `key={label}` inside
|
|
6269
6417
|
// `.map((label) => ...)` becoming a constant duplicate key).
|
|
6270
|
-
// `ctx.
|
|
6271
|
-
// names and index included
|
|
6272
|
-
|
|
6418
|
+
// `ctx.scope` is the live loop-binding stack (destructured binding
|
|
6419
|
+
// names and index included — plus, during the return-expression
|
|
6420
|
+
// transform window, the preamble's declared names once
|
|
6421
|
+
// `transformMapCall` re-enters the row scope with them, #2482 Stage 1a
|
|
6422
|
+
// Commit 2), so the guard is scope-accurate. This is a SHADOW-GUARD
|
|
6423
|
+
// query — reads `isBound` (all sources), not `valueBoundNames()`.
|
|
6424
|
+
if (ctx.scope.isBound(ident.text)) return null
|
|
6273
6425
|
const constInfo = findLocalConst(ident.text, ctx.analyzer)
|
|
6274
6426
|
if (!constInfo) return null
|
|
6275
6427
|
const ast = parseConstInitializer(constInfo)
|
|
@@ -6777,8 +6929,8 @@ function processComponentProps(
|
|
|
6777
6929
|
// the bare `classes` identifier with no prop refs, so the rewrite
|
|
6778
6930
|
// no-ops and the module-scope registration template leaks bare
|
|
6779
6931
|
// destructured props into `renderChild(...)` (#2468).
|
|
6780
|
-
const collapsed =
|
|
6781
|
-
const collapsedTemplate =
|
|
6932
|
+
const collapsed = templatePartsToJsExpr(value.parts)
|
|
6933
|
+
const collapsedTemplate = templatePartsToJsExpr(value.parts, { useTemplate: true })
|
|
6782
6934
|
value = AttrValueOf.expression(collapsed, {
|
|
6783
6935
|
parts: value.parts,
|
|
6784
6936
|
...(collapsedTemplate !== collapsed && { templateExpr: collapsedTemplate }),
|
|
@@ -6820,32 +6972,6 @@ function processComponentProps(
|
|
|
6820
6972
|
return props
|
|
6821
6973
|
}
|
|
6822
6974
|
|
|
6823
|
-
/**
|
|
6824
|
-
* Flatten a structured template-literal's parts back into a JS expression
|
|
6825
|
-
* string. Used at IR construction time when a structured `template` variant
|
|
6826
|
-
* needs to be collapsed into an `expression` for component-prop forwarding —
|
|
6827
|
-
* component props are runtime JS values, not HTML attribute bodies.
|
|
6828
|
-
*/
|
|
6829
|
-
function templatePartsToJsString(parts: readonly IRTemplatePart[], opts?: { useTemplate?: boolean }): string {
|
|
6830
|
-
let result = '`'
|
|
6831
|
-
for (const part of parts) {
|
|
6832
|
-
if (part.type === 'string') {
|
|
6833
|
-
result += (opts?.useTemplate && part.templateValue) ? part.templateValue : part.value
|
|
6834
|
-
} else if (part.type === 'ternary') {
|
|
6835
|
-
const cond = (opts?.useTemplate && part.templateCondition) ? part.templateCondition : part.condition
|
|
6836
|
-
result += `\${${cond} ? '${part.whenTrue}' : '${part.whenFalse}'}`
|
|
6837
|
-
} else if (part.type === 'lookup') {
|
|
6838
|
-
const key = (opts?.useTemplate && part.templateKey) ? part.templateKey : part.key
|
|
6839
|
-
const obj = '{' + Object.entries(part.cases).map(
|
|
6840
|
-
([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(v)}`
|
|
6841
|
-
).join(', ') + '}'
|
|
6842
|
-
result += `\${(${obj})[${key}]}`
|
|
6843
|
-
}
|
|
6844
|
-
}
|
|
6845
|
-
result += '`'
|
|
6846
|
-
return result
|
|
6847
|
-
}
|
|
6848
|
-
|
|
6849
6975
|
// =============================================================================
|
|
6850
6976
|
// Helpers
|
|
6851
6977
|
// =============================================================================
|
|
@@ -6969,10 +7095,15 @@ function isSignalOrMemoArray(array: string, ctx: TransformContext): boolean {
|
|
|
6969
7095
|
* Used by conditional transforms to assign slotId for per-item signal reactivity.
|
|
6970
7096
|
* NOT added to isReactiveExpression to avoid promoting text expressions
|
|
6971
7097
|
* like {item.name} to reactive (they use a separate slotId path).
|
|
7098
|
+
*
|
|
7099
|
+
* REACTIVITY/slotId classifier — reads `valueBoundNames()` (item/index/
|
|
7100
|
+
* destructure only), NOT preamble locals; see `BindingScope.valueBoundNames`'s
|
|
7101
|
+
* doc comment (#2482 Stage 1a Commit 2).
|
|
6972
7102
|
*/
|
|
6973
7103
|
function referencesLoopParam(expr: string, ctx: TransformContext): boolean {
|
|
6974
|
-
|
|
6975
|
-
|
|
7104
|
+
const boundNames = ctx.scope.valueBoundNames()
|
|
7105
|
+
if (boundNames.size === 0) return false
|
|
7106
|
+
for (const p of boundNames) {
|
|
6976
7107
|
if (new RegExp(`\\b${p}\\b`).test(expr)) return true
|
|
6977
7108
|
}
|
|
6978
7109
|
return false
|
|
@@ -7096,10 +7227,17 @@ function hasReactiveAttributes(attrs: IRAttribute[], ctx: TransformContext): boo
|
|
|
7096
7227
|
if (isSignalOrMemoReference(valueToCheck, ctx) || isPropsReference(valueToCheck, ctx)) {
|
|
7097
7228
|
return true
|
|
7098
7229
|
}
|
|
7099
|
-
// Check if attribute references any active loop
|
|
7100
|
-
// loop root elements need a slotId so className can be updated
|
|
7101
|
-
|
|
7102
|
-
|
|
7230
|
+
// Check if attribute references any active loop-bound VALUE names —
|
|
7231
|
+
// loop root elements need a slotId so className can be updated
|
|
7232
|
+
// reactively. REACTIVITY/slotId classifier — value bindings only
|
|
7233
|
+
// (item/index/destructure), not preamble locals; see
|
|
7234
|
+
// `BindingScope.valueBoundNames`'s doc comment (#2482 Stage 1a
|
|
7235
|
+
// Commit 2) — this is the exact check whose over-widening flipped
|
|
7236
|
+
// the `tag-cloud`/`preamble-cells` conformance fixtures before the
|
|
7237
|
+
// source-filtered query existed.
|
|
7238
|
+
const scopeValueNames = ctx.scope.valueBoundNames()
|
|
7239
|
+
if (scopeValueNames.size > 0) {
|
|
7240
|
+
for (const p of scopeValueNames) {
|
|
7103
7241
|
if (new RegExp(`\\b${p}\\b`).test(valueToCheck)) return true
|
|
7104
7242
|
}
|
|
7105
7243
|
}
|
package/src/module-exports.ts
CHANGED
|
@@ -21,10 +21,19 @@ export function generateModuleExports(
|
|
|
21
21
|
ir: ComponentIR,
|
|
22
22
|
extraInlineExported: ReadonlySet<string> = new Set(),
|
|
23
23
|
rewriteRelativeImport?: (importPath: string) => string,
|
|
24
|
+
options?: {
|
|
25
|
+
/**
|
|
26
|
+
* Skip `export const` / `export function` value declarations — the
|
|
27
|
+
* adapter already emitted them inside its module-scope section, in
|
|
28
|
+
* source order (see `TemplateSections.moduleConstantsIncludeExports`).
|
|
29
|
+
* `export { … } [from '…']` specifier blocks are still emitted.
|
|
30
|
+
*/
|
|
31
|
+
skipValueDeclarations?: boolean
|
|
32
|
+
},
|
|
24
33
|
): string | null {
|
|
25
34
|
const lines: string[] = []
|
|
26
35
|
|
|
27
|
-
for (const constant of ir.metadata.localConstants) {
|
|
36
|
+
for (const constant of options?.skipValueDeclarations ? [] : ir.metadata.localConstants) {
|
|
28
37
|
if (!constant.isExported) continue
|
|
29
38
|
const keyword = constant.declarationKind ?? 'const'
|
|
30
39
|
if (!constant.value) {
|
|
@@ -38,7 +47,7 @@ export function generateModuleExports(
|
|
|
38
47
|
lines.push(`export ${keyword} ${constant.name} = ${constant.value}`)
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
for (const func of ir.metadata.localFunctions) {
|
|
50
|
+
for (const func of options?.skipValueDeclarations ? [] : ir.metadata.localFunctions) {
|
|
42
51
|
if (!func.isExported) continue
|
|
43
52
|
// Prefer the source-verbatim signature so type predicates and explicit
|
|
44
53
|
// `:unknown` parameter annotations survive — see FunctionInfo.typedParams
|