@barefootjs/jsx 0.28.1 → 0.30.0
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/child-scope.d.ts +17 -0
- package/dist/adapters/child-scope.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +890 -357
- package/dist/ir-to-client-js/collect-elements.d.ts +5 -25
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts +66 -2
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.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/lazy-conditional.d.ts +92 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +134 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts +46 -11
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.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/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +14 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -3
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts +6 -0
- package/dist/ir-to-client-js/plan/declaration-emit.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +1 -10
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +12 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/rich-type-evidence.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value-references.d.ts +17 -4
- package/dist/value-references.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +6 -24
- package/src/__tests__/branch-loop-plain.test.ts +28 -45
- package/src/__tests__/csr-substitution-safety-divergence.test.ts +9 -3
- package/src/__tests__/csr-template-scope-soundness.test.ts +96 -0
- package/src/__tests__/early-return-branch-switch.test.ts +96 -0
- package/src/__tests__/form-control-value-ssr.test.ts +102 -0
- package/src/__tests__/ir-to-client-js/imports.test.ts +18 -0
- package/src/__tests__/lazy-conditional.test.ts +190 -0
- package/src/__tests__/lazy-preamble.test.ts +297 -0
- package/src/__tests__/lazy-row-eligibility.test.ts +97 -28
- package/src/__tests__/loop-item-root-scope.test.ts +213 -0
- package/src/__tests__/loop-row-creation-paths.test.ts +169 -0
- package/src/__tests__/map-multi-return-body.test.ts +30 -3
- package/src/__tests__/nested-loop-conditional.test.ts +27 -6
- package/src/__tests__/preamble-attr-reactivity.test.ts +217 -0
- package/src/__tests__/preamble-declarations.test.ts +207 -0
- package/src/__tests__/ssr-defaults.test.ts +44 -0
- package/src/__tests__/value-references.test.ts +88 -0
- package/src/adapters/child-scope.ts +22 -0
- package/src/adapters/loop-bound-names.ts +6 -0
- package/src/analyzer-context.ts +43 -0
- package/src/analyzer.ts +33 -3
- package/src/index.ts +1 -0
- package/src/ir-to-client-js/collect-elements.ts +51 -10
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +1 -1
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +3 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +2 -2
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +147 -5
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +2 -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/lazy-conditional.ts +151 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +287 -0
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +85 -22
- package/src/ir-to-client-js/control-flow/plan/reactive-effects.ts +7 -0
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +133 -35
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +24 -4
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +23 -4
- package/src/ir-to-client-js/csr-substitute.ts +6 -1
- package/src/ir-to-client-js/emit-registration.ts +1 -1
- package/src/ir-to-client-js/html-template.ts +51 -40
- package/src/ir-to-client-js/imports.ts +5 -0
- package/src/ir-to-client-js/index.ts +1 -1
- package/src/ir-to-client-js/plan/build-declaration-emit.ts +2 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +1 -1
- package/src/ir-to-client-js/plan/declaration-emit.ts +6 -0
- package/src/ir-to-client-js/reactivity.ts +25 -0
- package/src/ir-to-client-js/stringify/declaration-emit.ts +9 -4
- package/src/ir-to-client-js/types.ts +12 -0
- package/src/jsx-to-ir.ts +400 -24
- package/src/rich-type-evidence.ts +6 -7
- package/src/ssr-defaults.ts +7 -2
- package/src/to-locale-date-lowering.ts +8 -2
- package/src/types.ts +80 -0
- package/src/value-references.ts +26 -4
package/src/analyzer.ts
CHANGED
|
@@ -1201,10 +1201,21 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1201
1201
|
const callExpr = node.initializer as ts.CallExpression
|
|
1202
1202
|
|
|
1203
1203
|
const elements = pattern.elements
|
|
1204
|
+
// Getter-elided form: `const [, setActive] = createSignal(0)`. The first
|
|
1205
|
+
// element is a hole (ts.OmittedExpression), not a BindingElement.
|
|
1206
|
+
// Rejecting it here used to drop the whole declaration from the emitted
|
|
1207
|
+
// init while the setter stayed referenced by handlers — a guaranteed
|
|
1208
|
+
// ReferenceError (adapter-tests scope-gate, #2468 sweep). Accept it,
|
|
1209
|
+
// synthesize an internal getter name for the IR (never referenced by any
|
|
1210
|
+
// expression, so substitution/SSR consumers see an unused entry), and
|
|
1211
|
+
// mark `getterElided` so emit reproduces the source's `[, setter]` shape.
|
|
1212
|
+
const getterElided = elements.length === 2 && ts.isOmittedExpression(elements[0])
|
|
1204
1213
|
if (
|
|
1205
1214
|
elements.length < 1 || elements.length > 2 ||
|
|
1206
|
-
!
|
|
1207
|
-
|
|
1215
|
+
(!getterElided && (
|
|
1216
|
+
!ts.isBindingElement(elements[0]) ||
|
|
1217
|
+
!ts.isIdentifier(elements[0].name)
|
|
1218
|
+
))
|
|
1208
1219
|
) {
|
|
1209
1220
|
return
|
|
1210
1221
|
}
|
|
@@ -1216,10 +1227,11 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1216
1227
|
return
|
|
1217
1228
|
}
|
|
1218
1229
|
|
|
1219
|
-
const getter = elements[0].name.text
|
|
1220
1230
|
const setter = elements.length === 2 && ts.isBindingElement(elements[1]) && ts.isIdentifier(elements[1].name)
|
|
1221
1231
|
? elements[1].name.text
|
|
1222
1232
|
: null
|
|
1233
|
+
if (getterElided && !setter) return
|
|
1234
|
+
const getter = getterElided ? `__bfGet_${setter}` : (elements[0] as ts.BindingElement & { name: ts.Identifier }).name.text
|
|
1223
1235
|
const initialValue = callExpr.arguments[0] ? ctx.getJS(callExpr.arguments[0]) : ''
|
|
1224
1236
|
const typedInitialValue = callExpr.arguments[0] ? callExpr.arguments[0].getText(ctx.sourceFile) : undefined
|
|
1225
1237
|
|
|
@@ -1257,6 +1269,7 @@ function collectSignal(node: ts.VariableDeclaration, ctx: AnalyzerContext): void
|
|
|
1257
1269
|
ctx.signals.push({
|
|
1258
1270
|
getter,
|
|
1259
1271
|
setter,
|
|
1272
|
+
getterElided: getterElided || undefined,
|
|
1260
1273
|
initialValue,
|
|
1261
1274
|
typedInitialValue: typedInitialValue !== initialValue ? typedInitialValue : undefined,
|
|
1262
1275
|
templateInitialValue,
|
|
@@ -1577,9 +1590,26 @@ function collectMemo(node: ts.VariableDeclaration, ctx: AnalyzerContext): void {
|
|
|
1577
1590
|
const parsedBlockComplete =
|
|
1578
1591
|
parsedBlock && blockBody ? parsedBlock.length === blockBody.statements.length : undefined
|
|
1579
1592
|
|
|
1593
|
+
// Destructured-arg components only — the memo twin of the signal
|
|
1594
|
+
// `templateInitialValue` rewrite (#2265) a few hundred lines up: a memo
|
|
1595
|
+
// body referencing a bare destructured prop (`createMemo(() => value *
|
|
1596
|
+
// 10)` with `{ value }: { value: number }`) is inlined into the CSR
|
|
1597
|
+
// `template:` arrow by `buildSignalMemoEnv`, and that arrow runs at
|
|
1598
|
+
// module scope — it is not a closure over `initXxx`'s `const value =
|
|
1599
|
+
// _p.value` extraction, so the bare ref must become `_p.value`
|
|
1600
|
+
// (adapter-tests scope-gate, #2468 sweep).
|
|
1601
|
+
let templateComputation: string | undefined
|
|
1602
|
+
if (!ctx.propsObjectName && callExpr.arguments[0]) {
|
|
1603
|
+
const propNames = new Set(ctx.propsParams.map(p => p.name))
|
|
1604
|
+
if (propNames.size > 0) {
|
|
1605
|
+
templateComputation = rewriteBarePropRefs(computation, callExpr.arguments[0], propNames)
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1580
1609
|
ctx.memos.push({
|
|
1581
1610
|
name,
|
|
1582
1611
|
computation,
|
|
1612
|
+
templateComputation,
|
|
1583
1613
|
parsedBlock,
|
|
1584
1614
|
parsedBlockComplete,
|
|
1585
1615
|
typedComputation: typedComputation !== computation ? typedComputation : undefined,
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,7 @@ export { rewriteImportsForTemplate } from './adapters/template-imports.ts'
|
|
|
90
90
|
export { emitParsedExpr, groupBinaryOperand, isStringTypedOperand, isStringConcatBinary } from './adapters/parsed-expr-emitter.ts'
|
|
91
91
|
export type { ParsedExprEmitter, HigherOrderMethod, ArrayMethod, SortMethod, LiteralType } from './adapters/parsed-expr-emitter.ts'
|
|
92
92
|
export { collectLoopBoundNames } from './adapters/loop-bound-names.ts'
|
|
93
|
+
export { derivesScopeFromSlot } from './adapters/child-scope.ts'
|
|
93
94
|
export { evaluateSignalInit, tryEvaluateSignalInit, type SignalInitEvalResult } from './signal-init-eval.ts'
|
|
94
95
|
export { evaluateStaticLiteral, isFullyStaticLiteral, resolveStaticLoopSource } from './static-literal.ts'
|
|
95
96
|
export { importsSearchParams, searchParamsLocalNames, envSignalLocalNames, envSignalReaderFor, ENV_SIGNAL_READERS, queryHrefLocalNames, formatDateLocalNames, matchSearchParamsMethodCall } from './adapters/env-signal.ts'
|
|
@@ -328,10 +328,12 @@ export function collectInnerLoops(
|
|
|
328
328
|
// `NestedLoop` because event delegation handles them through
|
|
329
329
|
// the parent's bindings instead.
|
|
330
330
|
const bindings: LoopChildBindings = emptyLoopChildBindings()
|
|
331
|
+
// Hoisted: one Set per loop, not one per child (Copilot review).
|
|
332
|
+
const innerPreambleNames = preambleNamesOf(n)
|
|
331
333
|
if (ctx) {
|
|
332
334
|
for (const child of n.children) {
|
|
333
335
|
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, n.param, n.paramBindings))
|
|
334
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings))
|
|
336
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, n.param, n.paramBindings, false, innerPreambleNames))
|
|
335
337
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
336
338
|
}
|
|
337
339
|
}
|
|
@@ -669,7 +671,7 @@ export function collectElements(
|
|
|
669
671
|
const childHandlers: string[] = []
|
|
670
672
|
const bindings = projectionInner
|
|
671
673
|
? emptyLoopChildBindings()
|
|
672
|
-
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings)
|
|
674
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings, preambleNamesOf(l))
|
|
673
675
|
if (!projectionInner) {
|
|
674
676
|
for (const child of l.children) {
|
|
675
677
|
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
@@ -708,10 +710,12 @@ export function collectElements(
|
|
|
708
710
|
// Loop-param refs use the raw destructured binding (no loopParams
|
|
709
711
|
// passed), matching the plain-element path's `staticItemTemplate`.
|
|
710
712
|
if (l.isStaticArray && l.children[0]) {
|
|
711
|
-
// `
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
|
|
713
|
+
// `l.children[0]` is the loop's row-root component — already
|
|
714
|
+
// tagged `loopItemRoot` in the IR, so `derivesScopeFromSlot`
|
|
715
|
+
// drops the parent's slot suffix automatically. Each iteration
|
|
716
|
+
// owns a distinct scope identified by `data-key`, mirroring the
|
|
717
|
+
// SSR template's renderChild emit.
|
|
718
|
+
staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, undefined, undefined)
|
|
715
719
|
}
|
|
716
720
|
} else if (l.children[0] && !projectionInner) {
|
|
717
721
|
// Pass loopParams so expressions are wrapped at generation time,
|
|
@@ -728,8 +732,8 @@ export function collectElements(
|
|
|
728
732
|
// accessor wrap so the CSR materialize fallback (#1247) can clone
|
|
729
733
|
// items inside the forEach body without rewriting the template.
|
|
730
734
|
if (l.isStaticArray) {
|
|
731
|
-
// Plain-element body:
|
|
732
|
-
//
|
|
735
|
+
// Plain-element body: nested component nodes are never tagged
|
|
736
|
+
// `loopItemRoot`, so they keep their parent-slot context in the
|
|
733
737
|
// per-iteration renderChild call. Per #1249, the
|
|
734
738
|
// `outer-nested` static-array-child-init plan addresses
|
|
735
739
|
// children by the (bf-h, bf-m) pair against the enclosing
|
|
@@ -1139,7 +1143,7 @@ function collectBranchLoops(
|
|
|
1139
1143
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1140
1144
|
// no-op for existing items.
|
|
1141
1145
|
const branchBindings = ctx && !projectionInner
|
|
1142
|
-
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings)
|
|
1146
|
+
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings, preambleNamesOf(n))
|
|
1143
1147
|
: emptyLoopChildBindings()
|
|
1144
1148
|
|
|
1145
1149
|
loops.push({
|
|
@@ -1296,12 +1300,49 @@ function collectBranchConditionals(
|
|
|
1296
1300
|
* resolve child slot ids inside nested loops; the rest of the per-item
|
|
1297
1301
|
* collectors don't read them but pass them through for symmetry.
|
|
1298
1302
|
*/
|
|
1303
|
+
/**
|
|
1304
|
+
* The names a loop's `.map()` callback preamble declares, or `undefined` when
|
|
1305
|
+
* there are none to consider — the input to the attribute classifier's
|
|
1306
|
+
* preamble-local check (#2447 follow-up). `undefined` rather than an empty set
|
|
1307
|
+
* so the classifier's fast path stays a single `!== undefined` for the
|
|
1308
|
+
* overwhelmingly common preamble-free loop.
|
|
1309
|
+
*
|
|
1310
|
+
* A STATIC-ARRAY loop returns `undefined` even when it has a preamble, and
|
|
1311
|
+
* this is load-bearing rather than an optimisation. Its items are emitted
|
|
1312
|
+
* through `static-array-child-init` — a `forEach` over the SSR-rendered
|
|
1313
|
+
* children, with no per-row body and therefore no preamble in scope. Wiring an
|
|
1314
|
+
* attribute there would emit `setAttribute('class', active ? … : …)` against a
|
|
1315
|
+
* free variable: a ReferenceError at init that takes the whole component's
|
|
1316
|
+
* hydration with it, including any nested child. (Caught exactly that way —
|
|
1317
|
+
* the gallery nav shells are static arrays whose rows read
|
|
1318
|
+
* `const active = item.key === currentRoute`, and the badge components mounted
|
|
1319
|
+
* inside them stopped appearing.)
|
|
1320
|
+
*
|
|
1321
|
+
* Nothing is lost by declining: a static array's items are rendered once at
|
|
1322
|
+
* SSR and never recreated from a signal, so there is no same-key update for a
|
|
1323
|
+
* patch to be missing from. The same reasoning gates `markPreambleAttrSlots`
|
|
1324
|
+
* and `preambleRegions` in Phase 1, and the two gates must agree — a slot
|
|
1325
|
+
* granted without wiring, or wiring without a slot, is a silent hole either
|
|
1326
|
+
* way.
|
|
1327
|
+
*/
|
|
1328
|
+
function preambleNamesOf(loop: IRLoop): ReadonlySet<string> | undefined {
|
|
1329
|
+
if (loop.isStaticArray) return undefined
|
|
1330
|
+
const declared = loop.preamble?.declaredNames
|
|
1331
|
+
return declared && declared.length > 0 ? new Set(declared) : undefined
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1299
1334
|
export function collectLoopChildBindings(
|
|
1300
1335
|
children: readonly IRNode[],
|
|
1301
1336
|
ctx: ClientJsContext,
|
|
1302
1337
|
siblingOffsets: Map<IRLoop, IRNode[]>,
|
|
1303
1338
|
loopParam: string,
|
|
1304
1339
|
loopParamBindings: readonly import('../types.ts').LoopParamBinding[] | undefined,
|
|
1340
|
+
/**
|
|
1341
|
+
* Names the loop's `.map()` callback preamble declares, so an attribute
|
|
1342
|
+
* reading one is classified reactive (#2447 follow-up) — see
|
|
1343
|
+
* `collectLoopChildReactiveAttrs`. Omitted for a loop with no preamble.
|
|
1344
|
+
*/
|
|
1345
|
+
preambleNames?: ReadonlySet<string>,
|
|
1305
1346
|
): LoopChildBindings {
|
|
1306
1347
|
const bindings = emptyLoopChildBindings()
|
|
1307
1348
|
for (const child of children) {
|
|
@@ -1311,7 +1352,7 @@ export function collectLoopChildBindings(
|
|
|
1311
1352
|
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1312
1353
|
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1313
1354
|
// `.reactiveTexts`) — descending into them here too would double-bind.
|
|
1314
|
-
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true))
|
|
1355
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true, preambleNames))
|
|
1315
1356
|
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true))
|
|
1316
1357
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1317
1358
|
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
@@ -58,7 +58,7 @@ export function buildBranchLoopPlan(
|
|
|
58
58
|
const mapPreambleWrapped = loop.preamble
|
|
59
59
|
? renderPreamble(loop.preamble, {
|
|
60
60
|
transformJs: (t) => wrapLoopParamAsAccessor(t, loop.param, loop.paramBindings),
|
|
61
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined
|
|
61
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
|
|
62
62
|
})
|
|
63
63
|
: ''
|
|
64
64
|
const preambleRegions = buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings)
|
|
@@ -48,7 +48,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
48
48
|
mapPreambleWrapped: elem.preamble
|
|
49
49
|
? renderPreamble(elem.preamble, {
|
|
50
50
|
transformJs: wrap,
|
|
51
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined
|
|
51
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
52
52
|
})
|
|
53
53
|
: '',
|
|
54
54
|
template: elem.template,
|
|
@@ -108,7 +108,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
108
108
|
mapPreambleWrapped: loop.preamble
|
|
109
109
|
? renderPreamble(loop.preamble, {
|
|
110
110
|
transformJs: wrap,
|
|
111
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined
|
|
111
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined),
|
|
112
112
|
})
|
|
113
113
|
: '',
|
|
114
114
|
template: loop.template,
|
|
@@ -44,7 +44,7 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
44
44
|
// which throws since `t` is a plain object in this scope.
|
|
45
45
|
mapPreamble: elem.preamble
|
|
46
46
|
? renderPreamble(elem.preamble, {
|
|
47
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
47
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
48
48
|
})
|
|
49
49
|
: null,
|
|
50
50
|
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
@@ -76,7 +76,7 @@ export function buildBranchLoopDelegationPlan(
|
|
|
76
76
|
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
77
77
|
mapPreamble: loop.preamble
|
|
78
78
|
? renderPreamble(loop.preamble, {
|
|
79
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
79
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
80
80
|
})
|
|
81
81
|
: null,
|
|
82
82
|
mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
|
|
@@ -109,7 +109,7 @@ export function buildStaticArrayDelegationPlan(
|
|
|
109
109
|
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
110
110
|
mapPreamble: elem.preamble
|
|
111
111
|
? renderPreamble(elem.preamble, {
|
|
112
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
112
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
113
113
|
})
|
|
114
114
|
: null,
|
|
115
115
|
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
@@ -243,7 +243,7 @@ function buildReactiveEmit(
|
|
|
243
243
|
: [{ param: inner.param, bindings: inner.paramBindings }]
|
|
244
244
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
245
245
|
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
246
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined
|
|
246
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined),
|
|
247
247
|
}))
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -286,7 +286,7 @@ function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string
|
|
|
286
286
|
// of the js segments above).
|
|
287
287
|
if (inner.preamble) {
|
|
288
288
|
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
289
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined
|
|
289
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined),
|
|
290
290
|
}))
|
|
291
291
|
}
|
|
292
292
|
return {
|
|
@@ -20,8 +20,11 @@
|
|
|
20
20
|
import { freeIdentifiers, parseExpression } from '../../../expression-parser.ts'
|
|
21
21
|
import { pickAttrMeta, type AttrMeta } from '../../../types.ts'
|
|
22
22
|
import { extractFreeIdentifiersFromText } from '../../csr-substitute.ts'
|
|
23
|
+
import { addCondAttrToTemplate } from '../../html-template.ts'
|
|
23
24
|
import { wrapLoopParamAsAccessor, PROPS_PARAM } from '../../utils.ts'
|
|
24
25
|
import type { BranchLoop, ClientJsContext, TopLevelLoop } from '../../types.ts'
|
|
26
|
+
import { analyzeLazyConditional, type LazyConditionalFacts } from './lazy-conditional.ts'
|
|
27
|
+
import { analyzeLazyPreamble } from './lazy-preamble.ts'
|
|
25
28
|
import {
|
|
26
29
|
classifyLazyBinding,
|
|
27
30
|
lazyRowEligibility,
|
|
@@ -44,6 +47,13 @@ export interface LazyRowAttrBinding {
|
|
|
44
47
|
ordinal: number
|
|
45
48
|
readsItem: boolean
|
|
46
49
|
readsOuter: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Reads a `.map()` preamble local, so every apply body this binding is
|
|
52
|
+
* emitted into must re-run the preamble first (#2447 follow-up). Per
|
|
53
|
+
* binding, not per loop, so `applyOuter` does not pay for a preamble only
|
|
54
|
+
* `applyItem`'s bindings read.
|
|
55
|
+
*/
|
|
56
|
+
readsPreamble: boolean
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
/** One reactive text (content slot) of a lazy row. */
|
|
@@ -58,6 +68,37 @@ export interface LazyRowTextBinding {
|
|
|
58
68
|
* (`LazyRowPlanData.textNeedsRead`).
|
|
59
69
|
*/
|
|
60
70
|
readsOuter: boolean
|
|
71
|
+
/**
|
|
72
|
+
* Reads a `.map()` preamble local, so every apply body this binding is
|
|
73
|
+
* emitted into must re-run the preamble first (#2447 follow-up).
|
|
74
|
+
*/
|
|
75
|
+
readsPreamble: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One row conditional driven from the loop-level apply bodies (§9.5). */
|
|
79
|
+
export interface LazyRowConditionalBinding {
|
|
80
|
+
slotId: string
|
|
81
|
+
/** Already wrapped via `wrapLoopParamAsAccessor` — reads `<param>()`. */
|
|
82
|
+
wrappedCondition: string
|
|
83
|
+
whenTrueHtml: string
|
|
84
|
+
whenFalseHtml: string
|
|
85
|
+
/**
|
|
86
|
+
* Index into `entry.refs` holding the `[bf-c]` element. Always claimed ON
|
|
87
|
+
* DEMAND — `createRow` never seeds it, because the row it just cloned already
|
|
88
|
+
* rendered the correct arm and has no swap to perform.
|
|
89
|
+
*/
|
|
90
|
+
refIndex: number
|
|
91
|
+
/** Index into `entry.last` holding this conditional's dedup boolean. */
|
|
92
|
+
ordinal: number
|
|
93
|
+
readsItem: boolean
|
|
94
|
+
readsOuter: boolean
|
|
95
|
+
/**
|
|
96
|
+
* Reads a `.map()` preamble local, so every apply body this binding is
|
|
97
|
+
* emitted into must re-run the preamble first (#2447 follow-up). Per
|
|
98
|
+
* binding, not per loop, so `applyOuter` does not pay for a preamble only
|
|
99
|
+
* `applyItem`'s bindings read.
|
|
100
|
+
*/
|
|
101
|
+
readsPreamble: boolean
|
|
61
102
|
}
|
|
62
103
|
|
|
63
104
|
export interface LazyRowPlanData {
|
|
@@ -86,8 +127,35 @@ export interface LazyRowPlanData {
|
|
|
86
127
|
* nothing per row).
|
|
87
128
|
*/
|
|
88
129
|
outerPrimeGetters: readonly string[]
|
|
89
|
-
/**
|
|
90
|
-
|
|
130
|
+
/**
|
|
131
|
+
* The row's `.map()` callback preamble as already-wrapped JS statements
|
|
132
|
+
* (`mapPreambleWrapped`), or `''` when the row has none — proven safe to run
|
|
133
|
+
* by `analyzeLazyPreamble` (§9.5 widening).
|
|
134
|
+
*
|
|
135
|
+
* `createRow` always runs it, and specifically before the clone: the
|
|
136
|
+
* non-hoisted per-row template can interpolate values the preamble declares.
|
|
137
|
+
*
|
|
138
|
+
* `applyItem` / `applyOuter` run it only when a binding they own reads a
|
|
139
|
+
* declared local (`itemNeedsPreamble` / `outerNeedsPreamble`) — which is
|
|
140
|
+
* possible as of the #2447 follow-up, where an attribute reading one became
|
|
141
|
+
* a reactive binding instead of frozen template text. Re-running is sound
|
|
142
|
+
* because `analyzeLazyPreamble` proved the statements are `const`
|
|
143
|
+
* declarations whose initializers only read the item and zero-arg signal
|
|
144
|
+
* getters, and the binding's DEPENDENCIES are the preamble's own free
|
|
145
|
+
* identifiers (substituted in `classifyLazyBinding`), so `applyOuter`
|
|
146
|
+
* primes what the preamble reads rather than the opaque local name.
|
|
147
|
+
*/
|
|
148
|
+
preambleStatements: string
|
|
149
|
+
/** `applyItem` has a binding that reads a preamble local (#2447 follow-up). */
|
|
150
|
+
itemNeedsPreamble: boolean
|
|
151
|
+
/** `applyOuter` has one. Separate from the above so neither body pays for the other's. */
|
|
152
|
+
outerNeedsPreamble: boolean
|
|
153
|
+
/**
|
|
154
|
+
* Row conditionals driven from the apply bodies (§9.5, `lazy-conditional.ts`).
|
|
155
|
+
* Empty for most loops. Each arm is a static element, so the emitter hoists
|
|
156
|
+
* both parsed once per loop and clones on a flip.
|
|
157
|
+
*/
|
|
158
|
+
conditionals: readonly LazyRowConditionalBinding[]
|
|
91
159
|
}
|
|
92
160
|
|
|
93
161
|
export interface BuildLazyRowArgs {
|
|
@@ -126,6 +194,28 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
126
194
|
const rowLocalNames = new Set<string>([loop.param])
|
|
127
195
|
for (const b of loop.paramBindings ?? []) rowLocalNames.add(b.name)
|
|
128
196
|
|
|
197
|
+
// §9.5 preamble widening: prove the preamble is safe to re-run in the apply
|
|
198
|
+
// bodies BEFORE classifying, because a binding that reads a declared local
|
|
199
|
+
// inherits the preamble's dependencies rather than its own literal names.
|
|
200
|
+
const primableNames = new Set<string>([...scope.signals.keys(), ...scope.memos])
|
|
201
|
+
const preambleAnalysis = analyzeLazyPreamble(loop.preamble, args.indexParam, primableNames)
|
|
202
|
+
|
|
203
|
+
// §9.5 conditional widening: a row conditional whose arms are wiring-free
|
|
204
|
+
// static elements is driven from the apply bodies instead of a per-row
|
|
205
|
+
// `insert()` effect. The FIRST refusal wins, so the gate names one shape.
|
|
206
|
+
const rawConditionals = loop.bindings.conditionals ?? []
|
|
207
|
+
const condFacts: LazyConditionalFacts[] = []
|
|
208
|
+
let conditionalRefusal: string | null = null
|
|
209
|
+
for (const cond of rawConditionals) {
|
|
210
|
+
const verdict = analyzeLazyConditional(cond, args.indexParam, {
|
|
211
|
+
whenTrueHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
212
|
+
whenFalseHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
213
|
+
})
|
|
214
|
+
if (!verdict.lazySafe) { conditionalRefusal = verdict.reason; break }
|
|
215
|
+
condFacts.push(verdict.facts)
|
|
216
|
+
}
|
|
217
|
+
const preambleFacts = preambleAnalysis.lazySafe ? preambleAnalysis.facts : undefined
|
|
218
|
+
|
|
129
219
|
// --- classify every binding -------------------------------------------
|
|
130
220
|
const classified: ClassifiedLazyBinding[] = []
|
|
131
221
|
const attrClass = new Map<number, ClassifiedLazyBinding>()
|
|
@@ -137,6 +227,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
137
227
|
rowLocalNames,
|
|
138
228
|
indexParam: args.indexParam,
|
|
139
229
|
scope,
|
|
230
|
+
preamble: preambleFacts,
|
|
140
231
|
})
|
|
141
232
|
attrClass.set(i, c)
|
|
142
233
|
classified.push(c)
|
|
@@ -150,11 +241,30 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
150
241
|
rowLocalNames,
|
|
151
242
|
indexParam: args.indexParam,
|
|
152
243
|
scope,
|
|
244
|
+
preamble: preambleFacts,
|
|
153
245
|
})
|
|
154
246
|
textClass.set(i, c)
|
|
155
247
|
classified.push(c)
|
|
156
248
|
})
|
|
157
249
|
|
|
250
|
+
const condClass = new Map<number, ClassifiedLazyBinding>()
|
|
251
|
+
condFacts.forEach((c, i) => {
|
|
252
|
+
// Classified like any other binding: a condition reading an outer signal
|
|
253
|
+
// must land in `applyOuter` AND get that signal onto the prime list, or the
|
|
254
|
+
// loop-level effect would never subscribe to it.
|
|
255
|
+
const k = classifyLazyBinding({
|
|
256
|
+
kind: 'attr',
|
|
257
|
+
slotId: c.slotId,
|
|
258
|
+
free: rawConditionals[i].conditionFreeIdentifiers ?? null,
|
|
259
|
+
rowLocalNames,
|
|
260
|
+
indexParam: args.indexParam,
|
|
261
|
+
scope,
|
|
262
|
+
preamble: preambleFacts,
|
|
263
|
+
})
|
|
264
|
+
condClass.set(i, k)
|
|
265
|
+
classified.push(k)
|
|
266
|
+
})
|
|
267
|
+
|
|
158
268
|
// --- §9.4 gate ---------------------------------------------------------
|
|
159
269
|
const shape: LazyRowShapeFacts = {
|
|
160
270
|
callSite: args.callSite,
|
|
@@ -162,12 +272,12 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
162
272
|
anchored: args.anchored,
|
|
163
273
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
164
274
|
hasExplicitKey: loop.key != null,
|
|
165
|
-
|
|
275
|
+
conditionalRefusal,
|
|
166
276
|
childRefCount: loop.bindings.refs?.length ?? 0,
|
|
167
277
|
nestedComponentCount: loop.nestedComponents?.length ?? 0,
|
|
168
278
|
innerLoopCount: loop.innerLoops?.length ?? 0,
|
|
169
279
|
hasChildComponent: 'childComponent' in loop && loop.childComponent != null,
|
|
170
|
-
|
|
280
|
+
mapPreambleRefusal: preambleAnalysis.lazySafe ? null : preambleAnalysis.reason,
|
|
171
281
|
preambleRegionCount: args.preambleRegionCount,
|
|
172
282
|
hasParamUnwrap: args.paramUnwrap.length > 0,
|
|
173
283
|
}
|
|
@@ -198,6 +308,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
198
308
|
ordinal: ordinal++,
|
|
199
309
|
readsItem: c.readsItem,
|
|
200
310
|
readsOuter: c.readsOuter,
|
|
311
|
+
readsPreamble: c.readsPreamble,
|
|
201
312
|
}
|
|
202
313
|
})
|
|
203
314
|
const texts: LazyRowTextBinding[] = loop.bindings.reactiveTexts.map((text, i) => {
|
|
@@ -212,6 +323,7 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
212
323
|
// a list keeps exactly the classifier's answer.
|
|
213
324
|
readsItem: c.readsItem || !c.readsOuter,
|
|
214
325
|
readsOuter: c.readsOuter,
|
|
326
|
+
readsPreamble: c.readsPreamble,
|
|
215
327
|
}
|
|
216
328
|
})
|
|
217
329
|
|
|
@@ -222,6 +334,29 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
222
334
|
}
|
|
223
335
|
}
|
|
224
336
|
|
|
337
|
+
// Conditional refs live PAST the door, and are claimed on demand — the row
|
|
338
|
+
// `createRow` just cloned already rendered the correct arm, so there is
|
|
339
|
+
// nothing for it to seed or swap (see `LazyRowConditionalBinding.refIndex`).
|
|
340
|
+
const condRefBase = attrSlotIds.length + (texts.length > 0 ? 1 : 0)
|
|
341
|
+
const conditionals: LazyRowConditionalBinding[] = condFacts.map((c, i) => {
|
|
342
|
+
const klass = condClass.get(i)!
|
|
343
|
+
return {
|
|
344
|
+
slotId: c.slotId,
|
|
345
|
+
wrappedCondition: wrap(c.condition),
|
|
346
|
+
whenTrueHtml: c.whenTrueHtml,
|
|
347
|
+
whenFalseHtml: c.whenFalseHtml,
|
|
348
|
+
refIndex: condRefBase + i,
|
|
349
|
+
ordinal: ordinal++,
|
|
350
|
+
// A condition reading neither the item nor a reactive outer name still
|
|
351
|
+
// has to be applied somewhere; `applyItem` is the harmless choice (the
|
|
352
|
+
// dedup makes a repeat a no-op), matching how a text that classified as
|
|
353
|
+
// neither is handled above.
|
|
354
|
+
readsItem: klass.readsItem || !klass.readsOuter,
|
|
355
|
+
readsOuter: klass.readsOuter,
|
|
356
|
+
readsPreamble: klass.readsPreamble,
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
225
360
|
return {
|
|
226
361
|
plan: {
|
|
227
362
|
attrSlotIds,
|
|
@@ -231,7 +366,14 @@ export function decideLazyRow(args: BuildLazyRowArgs): {
|
|
|
231
366
|
textNeedsRead: texts.some(t => t.readsOuter),
|
|
232
367
|
lastCount: ordinal,
|
|
233
368
|
outerPrimeGetters,
|
|
234
|
-
|
|
369
|
+
preambleStatements: args.mapPreambleWrapped,
|
|
370
|
+
// Which apply bodies must re-run the preamble — computed from the FINAL
|
|
371
|
+
// binding lists, not the raw classification, so a text/conditional that
|
|
372
|
+
// classified as neither item- nor outer-driven (and was therefore forced
|
|
373
|
+
// into `applyItem` above) is counted in the body it actually lands in.
|
|
374
|
+
itemNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsItem && b.readsPreamble),
|
|
375
|
+
outerNeedsPreamble: [...attrs, ...texts, ...conditionals].some(b => b.readsOuter && b.readsPreamble),
|
|
376
|
+
conditionals,
|
|
235
377
|
},
|
|
236
378
|
decision,
|
|
237
379
|
}
|
|
@@ -144,7 +144,7 @@ export function buildPlainLoopPlan(
|
|
|
144
144
|
const mapPreambleWrapped = elem.preamble
|
|
145
145
|
? renderPreamble(elem.preamble, {
|
|
146
146
|
transformJs: wrap,
|
|
147
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined
|
|
147
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
148
148
|
})
|
|
149
149
|
: ''
|
|
150
150
|
const preambleRegions = buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings)
|
|
@@ -269,7 +269,7 @@ function buildStaticLoopMaterialize(
|
|
|
269
269
|
itemTemplate: elem.staticItemTemplate,
|
|
270
270
|
mapPreamble: elem.preamble
|
|
271
271
|
? renderPreamble(elem.preamble, {
|
|
272
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined
|
|
272
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined),
|
|
273
273
|
})
|
|
274
274
|
: '',
|
|
275
275
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|