@barefootjs/jsx 0.26.3 → 0.26.4
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/index.js +433 -55
- package/dist/ir-to-client-js/build-references.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/branch-loop.d.ts +12 -1
- package/dist/ir-to-client-js/control-flow/plan/branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-branch-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts +16 -1
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts +27 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +12 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +49 -1
- 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/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +26 -1
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +187 -37
- package/src/__tests__/client-js-generation.test.ts +8 -2
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/flatmap-segments.test.ts +182 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +45 -0
- package/src/__tests__/preamble-region-patch.test.ts +150 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/ir-to-client-js/build-references.ts +16 -0
- package/src/ir-to-client-js/collect-elements.ts +66 -10
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +12 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +11 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +19 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +36 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +16 -1
- package/src/ir-to-client-js/control-flow/plan/loop.ts +28 -0
- package/src/ir-to-client-js/control-flow/shared.ts +26 -1
- package/src/ir-to-client-js/control-flow/stringify/branch-loop.ts +25 -3
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +67 -19
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
- package/src/ir-to-client-js/html-template.ts +115 -6
- package/src/ir-to-client-js/imports.ts +1 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/types.ts +24 -0
- package/src/jsx-to-ir.ts +374 -8
- package/src/types.ts +49 -0
|
@@ -6,7 +6,7 @@ import { type IRNode, type IRElement, type IRComponent, type IRLoop, type IRProp
|
|
|
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
8
|
import { classifyReactivity, decideWrapForAttr, decideWrapForChildProp, decideWrapFromAstFlags, collectEventHandlersFromIR, collectConditionalBranchEvents, collectConditionalBranchRefs, collectConditionalBranchChildComponents, collectLoopChildEventsWithNesting, collectLoopChildReactiveAttrs, collectLoopChildReactiveTexts, collectLoopChildRefs, emptyLoopChildBindings } from './reactivity.ts'
|
|
9
|
-
import { irToHtmlTemplate, irToPlaceholderTemplate, irChildrenToJsExpr, buildLoopSkeletonTemplate, computeSkeletonSlotPaths, type SkeletonSlotPaths } from './html-template.ts'
|
|
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
12
|
import { walkIR, stopAt } from './walker.ts'
|
|
@@ -650,10 +650,26 @@ export function collectElements(
|
|
|
650
650
|
// are extracted explicitly below for the closure capture set.
|
|
651
651
|
if (!l.slotId || inCond) return
|
|
652
652
|
|
|
653
|
+
// flatMap PROJECTION loop: the only child is a nested IRLoop lowered
|
|
654
|
+
// from `flatMap(it => it.tags.map(...))`. The children exist for SSR
|
|
655
|
+
// templatization (nested {{range}} on DSL adapters); the CLIENT
|
|
656
|
+
// reconciles the flattened leaves through the descriptor mapArray
|
|
657
|
+
// path synthesized from the same inner loop, so none of the
|
|
658
|
+
// children-derived client machinery (bindings, inner-loop plans,
|
|
659
|
+
// per-row template) applies — leaf wiring was refused at IR build.
|
|
660
|
+
const projectionInner =
|
|
661
|
+
l.method === 'flatMap' && l.children.length === 1 && l.children[0].type === 'loop'
|
|
662
|
+
? l.children[0]
|
|
663
|
+
: undefined
|
|
664
|
+
|
|
653
665
|
const childHandlers: string[] = []
|
|
654
|
-
const bindings =
|
|
655
|
-
|
|
656
|
-
|
|
666
|
+
const bindings = projectionInner
|
|
667
|
+
? emptyLoopChildBindings()
|
|
668
|
+
: collectLoopChildBindings(l.children, ctx, siblingOffsets, l.param, l.paramBindings)
|
|
669
|
+
if (!projectionInner) {
|
|
670
|
+
for (const child of l.children) {
|
|
671
|
+
childHandlers.push(...collectEventHandlersFromIR(child))
|
|
672
|
+
}
|
|
657
673
|
}
|
|
658
674
|
|
|
659
675
|
if (l.childComponent) {
|
|
@@ -668,7 +684,9 @@ export function collectElements(
|
|
|
668
684
|
// Determine rendering strategy for dynamic arrays:
|
|
669
685
|
// Use element reconciliation when the loop body has nested components,
|
|
670
686
|
// or when inner loops need their own mapArray for events/reactive text.
|
|
671
|
-
const { useElementReconciliation, innerLoops } =
|
|
687
|
+
const { useElementReconciliation, innerLoops } = projectionInner
|
|
688
|
+
? { useElementReconciliation: false, innerLoops: undefined }
|
|
689
|
+
: decideLoopRendering(l, siblingOffsets, ctx)
|
|
672
690
|
|
|
673
691
|
let template = ''
|
|
674
692
|
let staticItemTemplate: string | undefined
|
|
@@ -691,7 +709,7 @@ export function collectElements(
|
|
|
691
709
|
// `data-key`, mirroring the SSR template's renderChild emit.
|
|
692
710
|
staticItemTemplate = irToHtmlTemplate(l.children[0], buildRestSpreadNames(ctx), 0, undefined, undefined, /* insideLoop */ true)
|
|
693
711
|
}
|
|
694
|
-
} else if (l.children[0]) {
|
|
712
|
+
} else if (l.children[0] && !projectionInner) {
|
|
695
713
|
// Pass loopParams so expressions are wrapped at generation time,
|
|
696
714
|
// avoiding post-hoc regex wrapping that corrupts literal attribute values.
|
|
697
715
|
// Forward destructured bindings (#951) so references like `cfg.color`
|
|
@@ -777,6 +795,20 @@ export function collectElements(
|
|
|
777
795
|
} : undefined,
|
|
778
796
|
chainOrder: l.chainOrder,
|
|
779
797
|
preamble: l.preamble,
|
|
798
|
+
preambleRegions: l.preambleRegions,
|
|
799
|
+
flatMapClient: projectionInner
|
|
800
|
+
? {
|
|
801
|
+
params: l.index ? `(${l.param}, ${l.index})` : `(${l.param})`,
|
|
802
|
+
body: renderFlatMapProjectionClientBody(projectionInner, buildRestSpreadNames(ctx)),
|
|
803
|
+
keyed: projectionInner.key !== null,
|
|
804
|
+
}
|
|
805
|
+
: l.flatMapCallback
|
|
806
|
+
? {
|
|
807
|
+
params: l.flatMapCallback.params,
|
|
808
|
+
body: renderFlatMapClientBody(l.flatMapCallback, buildRestSpreadNames(ctx)),
|
|
809
|
+
keyed: flatMapCallbackHasKeyedLeaf(l.flatMapCallback),
|
|
810
|
+
}
|
|
811
|
+
: undefined,
|
|
780
812
|
})
|
|
781
813
|
// Don't descend — loop-scoped variables are only available inside the iteration.
|
|
782
814
|
},
|
|
@@ -1069,8 +1101,16 @@ function collectBranchLoops(
|
|
|
1069
1101
|
// reactive-text collection on inner loops reached from this call path
|
|
1070
1102
|
// is handled at the enclosing branch-loop level below (`if (ctx)` block
|
|
1071
1103
|
// around `collectLoopChildReactiveTexts`), not per inner loop.
|
|
1072
|
-
|
|
1073
|
-
|
|
1104
|
+
// flatMap PROJECTION loop — see the top-level handler's note: children
|
|
1105
|
+
// are SSR-only; the client rides the descriptor mapArray path.
|
|
1106
|
+
const projectionInner =
|
|
1107
|
+
n.method === 'flatMap' && n.children.length === 1 && n.children[0].type === 'loop'
|
|
1108
|
+
? n.children[0]
|
|
1109
|
+
: undefined
|
|
1110
|
+
|
|
1111
|
+
const { useElementReconciliation, innerLoops: innerLoopsCollected } = projectionInner
|
|
1112
|
+
? { useElementReconciliation: false, innerLoops: undefined }
|
|
1113
|
+
: decideLoopRendering(n, siblingOffsets, undefined)
|
|
1074
1114
|
|
|
1075
1115
|
// Build the item template from loop children.
|
|
1076
1116
|
// Use loopDepth=0: this loop gets its own reconcileElements (independent
|
|
@@ -1080,7 +1120,9 @@ function collectBranchLoops(
|
|
|
1080
1120
|
// use `param()` to read the current item value.
|
|
1081
1121
|
let childTemplate: string
|
|
1082
1122
|
const branchLoopParamSpec = [{ param: n.param, bindings: n.paramBindings }]
|
|
1083
|
-
if (
|
|
1123
|
+
if (projectionInner) {
|
|
1124
|
+
childTemplate = '' // descriptor renderItem builds from d.h, not a row template
|
|
1125
|
+
} else if (useElementReconciliation && n.children[0]) {
|
|
1084
1126
|
childTemplate = irToPlaceholderTemplate(n.children[0], restNames, 0, branchLoopParamSpec)
|
|
1085
1127
|
} else {
|
|
1086
1128
|
childTemplate = n.children.map(c => irToHtmlTemplate(c, undefined, 0, branchLoopParamSpec)).join('')
|
|
@@ -1092,7 +1134,7 @@ function collectBranchLoops(
|
|
|
1092
1134
|
// memos). Previously these were only collected for composite loops,
|
|
1093
1135
|
// which caused reactive reads inside simple loop bodies to silently
|
|
1094
1136
|
// no-op for existing items.
|
|
1095
|
-
const branchBindings = ctx
|
|
1137
|
+
const branchBindings = ctx && !projectionInner
|
|
1096
1138
|
? collectLoopChildBindings(n.children, ctx, siblingOffsets, n.param, n.paramBindings)
|
|
1097
1139
|
: emptyLoopChildBindings()
|
|
1098
1140
|
|
|
@@ -1112,6 +1154,7 @@ function collectBranchLoops(
|
|
|
1112
1154
|
template: childTemplate,
|
|
1113
1155
|
containerSlotId: containerSlot,
|
|
1114
1156
|
preamble: n.preamble,
|
|
1157
|
+
preambleRegions: n.preambleRegions,
|
|
1115
1158
|
nestedComponents: useElementReconciliation ? n.nestedComponents : undefined,
|
|
1116
1159
|
bindings: branchBindings,
|
|
1117
1160
|
innerLoops: useElementReconciliation ? innerLoopsCollected : undefined,
|
|
@@ -1126,6 +1169,19 @@ function collectBranchLoops(
|
|
|
1126
1169
|
raw: n.sortComparator.raw,
|
|
1127
1170
|
} : undefined,
|
|
1128
1171
|
chainOrder: n.chainOrder,
|
|
1172
|
+
flatMapClient: projectionInner
|
|
1173
|
+
? {
|
|
1174
|
+
params: n.index ? `(${n.param}, ${n.index})` : `(${n.param})`,
|
|
1175
|
+
body: renderFlatMapProjectionClientBody(projectionInner, restNames),
|
|
1176
|
+
keyed: projectionInner.key !== null,
|
|
1177
|
+
}
|
|
1178
|
+
: n.flatMapCallback
|
|
1179
|
+
? {
|
|
1180
|
+
params: n.flatMapCallback.params,
|
|
1181
|
+
body: renderFlatMapClientBody(n.flatMapCallback, restNames),
|
|
1182
|
+
keyed: flatMapCallbackHasKeyedLeaf(n.flatMapCallback),
|
|
1183
|
+
}
|
|
1184
|
+
: undefined,
|
|
1129
1185
|
})
|
|
1130
1186
|
// Don't recurse into the loop — nested loops are handled by the loop's own reconciliation.
|
|
1131
1187
|
},
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
* pair.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { CompositeLoopPlan, LoopChildRefBinding } from './loop.ts'
|
|
18
|
+
import type { CompositeLoopPlan, LoopChildRefBinding, PreambleRegionPlan } from './loop.ts'
|
|
19
19
|
import type { EventDelegationPlan } from './event-delegation.ts'
|
|
20
20
|
import type { ReactiveEffectsPlan } from './reactive-effects.ts'
|
|
21
21
|
|
|
22
22
|
export interface BranchPlainLoopPlan {
|
|
23
|
+
/**
|
|
24
|
+
* flatMap descriptor mode — see `PlainLoopVariant.flatMapLeafItem`
|
|
25
|
+
* (plan/loop.ts): `arrayExpr` already flattens through the descriptor
|
|
26
|
+
* body and the stringifier emits the build-or-patch renderItem.
|
|
27
|
+
*/
|
|
28
|
+
flatMapLeafItem?: boolean
|
|
23
29
|
kind: 'plain'
|
|
24
30
|
/** See LoopPlanCommon.rowConstruction — required on every variant. */
|
|
25
31
|
rowConstruction: 'string-template' | 'dom-ops'
|
|
@@ -78,6 +84,11 @@ export interface BranchPlainLoopPlan {
|
|
|
78
84
|
* Undefined off → byte-identical (SR8).
|
|
79
85
|
*/
|
|
80
86
|
profileLoopId?: string
|
|
87
|
+
/**
|
|
88
|
+
* Preamble-patched regions (#2389) — non-empty forces the multi-line
|
|
89
|
+
* renderItem layout, mirroring the top-level plain plan.
|
|
90
|
+
*/
|
|
91
|
+
preambleRegions: readonly PreambleRegionPlan[]
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
export interface BranchCompositeLoopPlan {
|
|
@@ -15,7 +15,7 @@ import { buildChainedArrayExpr, varSlotId, wrapLoopParamAsAccessor } from '../..
|
|
|
15
15
|
import { buildBranchCompositePlan } from './build-composite-loop.ts'
|
|
16
16
|
import { buildBranchLoopDelegationPlan } from './build-event-delegation.ts'
|
|
17
17
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
18
|
-
import { destructureLoopParam, loopKeyFn, buildChildRefBindings } from '../shared.ts'
|
|
18
|
+
import { destructureLoopParam, loopKeyFn, buildChildRefBindings, buildPreambleRegionPlans } from '../shared.ts'
|
|
19
19
|
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
20
20
|
import type {
|
|
21
21
|
BranchCompositeLoopPlan,
|
|
@@ -43,14 +43,21 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
43
43
|
|| loop.bindings.reactiveTexts.length > 0
|
|
44
44
|
|| loop.bindings.conditionals.length > 0
|
|
45
45
|
|
|
46
|
+
// flatMap descriptor mode — see buildPlainLoopPlan (build-loop.ts).
|
|
47
|
+
const fm = loop.flatMapClient
|
|
46
48
|
const plan: BranchPlainLoopPlan = {
|
|
47
49
|
kind: 'plain',
|
|
48
50
|
rowConstruction: 'string-template',
|
|
49
51
|
containerSlotId,
|
|
50
52
|
containerVar,
|
|
51
53
|
markerId: loop.markerId,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
flatMapLeafItem: fm ? true : undefined,
|
|
55
|
+
arrayExpr: fm
|
|
56
|
+
? `(${buildChainedArrayExpr(loop)}).flatMap(${fm.params} => ${fm.body})`
|
|
57
|
+
: buildChainedArrayExpr(loop),
|
|
58
|
+
keyFn: fm
|
|
59
|
+
? (fm.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null')
|
|
60
|
+
: loopKeyFn(loop),
|
|
54
61
|
paramHead,
|
|
55
62
|
paramUnwrap,
|
|
56
63
|
indexParam: loop.index || '__idx',
|
|
@@ -75,6 +82,7 @@ export function buildBranchLoopPlan(loop: BranchLoop, profileComponentName?: str
|
|
|
75
82
|
: null,
|
|
76
83
|
eventDelegation: buildBranchLoopDelegationPlan(loop, cv, profileComponentName),
|
|
77
84
|
childRefs: buildChildRefBindings(loop.bindings.refs, loop.param, loop.paramBindings),
|
|
85
|
+
preambleRegions: buildPreambleRegionPlans(loop.preambleRegions, loop.param, loop.paramBindings),
|
|
78
86
|
bodyIsMultiRoot: loop.bodyIsMultiRoot ?? false,
|
|
79
87
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${containerSlotId}` : undefined,
|
|
80
88
|
}
|
|
@@ -36,11 +36,18 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
36
36
|
paramBindings: elem.paramBindings,
|
|
37
37
|
key: elem.key,
|
|
38
38
|
index: elem.index,
|
|
39
|
+
// No loopParams spec here (unlike the row-render context) — in the
|
|
40
|
+
// delegated handler `elem.param` is bound to the plain `.find()`/
|
|
41
|
+
// indexed result, not a signal accessor, so leaf refs must stay in
|
|
42
|
+
// their literal (`t.name`) form. Passing a loopParams spec here was
|
|
43
|
+
// BUG-3: it rewrote leaf refs to accessor-call form (`t().name`),
|
|
44
|
+
// which throws since `t` is a plain object in this scope.
|
|
39
45
|
mapPreamble: elem.preamble
|
|
40
46
|
? renderPreamble(elem.preamble, {
|
|
41
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1,
|
|
47
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
42
48
|
})
|
|
43
49
|
: null,
|
|
50
|
+
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
44
51
|
}),
|
|
45
52
|
}
|
|
46
53
|
}
|
|
@@ -65,11 +72,14 @@ export function buildBranchLoopDelegationPlan(
|
|
|
65
72
|
paramBindings: loop.paramBindings,
|
|
66
73
|
key: loop.key,
|
|
67
74
|
index: loop.index,
|
|
75
|
+
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
76
|
+
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
68
77
|
mapPreamble: loop.preamble
|
|
69
78
|
? renderPreamble(loop.preamble, {
|
|
70
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1,
|
|
79
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
71
80
|
})
|
|
72
81
|
: null,
|
|
82
|
+
mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
|
|
73
83
|
}),
|
|
74
84
|
}
|
|
75
85
|
}
|
|
@@ -95,11 +105,14 @@ export function buildStaticArrayDelegationPlan(
|
|
|
95
105
|
// array too (#1434).
|
|
96
106
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
97
107
|
param: elem.param,
|
|
108
|
+
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
109
|
+
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
98
110
|
mapPreamble: elem.preamble
|
|
99
111
|
? renderPreamble(elem.preamble, {
|
|
100
|
-
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1,
|
|
112
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
101
113
|
})
|
|
102
114
|
: null,
|
|
115
|
+
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
103
116
|
offset: elem.offset ?? null,
|
|
104
117
|
indexParam: elem.index ?? null,
|
|
105
118
|
},
|
|
@@ -117,6 +130,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
117
130
|
key: string | null
|
|
118
131
|
index: string | null
|
|
119
132
|
mapPreamble: string | null
|
|
133
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
120
134
|
}): ItemLookup {
|
|
121
135
|
const hasBindings = (args.paramBindings?.length ?? 0) > 0
|
|
122
136
|
if (args.key !== null) {
|
|
@@ -132,6 +146,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
132
146
|
paramBindings: args.paramBindings,
|
|
133
147
|
keyWithItem,
|
|
134
148
|
mapPreamble: args.mapPreamble,
|
|
149
|
+
mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
|
|
135
150
|
hasBindings,
|
|
136
151
|
indexParam: args.index,
|
|
137
152
|
}
|
|
@@ -141,6 +156,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
141
156
|
arrayExpr: args.array,
|
|
142
157
|
param: args.param,
|
|
143
158
|
mapPreamble: args.mapPreamble,
|
|
159
|
+
mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
|
|
144
160
|
hasBindings,
|
|
145
161
|
indexParam: args.index,
|
|
146
162
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
destructureLoopParam,
|
|
36
36
|
buildChildRefBindings,
|
|
37
37
|
buildStaticChildRefBindings,
|
|
38
|
+
buildPreambleRegionPlans,
|
|
38
39
|
} from '../shared.ts'
|
|
39
40
|
import { buildLoopReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
40
41
|
import { buildComponentLoopPlan } from './build-component-loop.ts'
|
|
@@ -91,6 +92,40 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
91
92
|
|| elem.bindings.reactiveTexts.length > 0
|
|
92
93
|
|| elem.bindings.conditionals.length > 0
|
|
93
94
|
|
|
95
|
+
// flatMap descriptor mode: reconcile the FLATTENED leaves. The source
|
|
96
|
+
// accessor runs the flatMap body (plain items — per-item signals don't
|
|
97
|
+
// exist yet) producing `({ k, h })` descriptors; the keyFn reads the
|
|
98
|
+
// leaf-declared key with an index fallback. Everything row-template-
|
|
99
|
+
// related is bypassed — the stringifier emits the build-or-patch
|
|
100
|
+
// renderItem instead. Pre-fix this fell into the generic plain shape
|
|
101
|
+
// with the UN-flattened source, a null keyFn, and an EMPTY template
|
|
102
|
+
// (leaf loss at hydration; cloneNode(null) crash on adds).
|
|
103
|
+
if (elem.flatMapClient) {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'plain',
|
|
106
|
+
rowConstruction: 'string-template',
|
|
107
|
+
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
108
|
+
markerId: elem.markerId,
|
|
109
|
+
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
110
|
+
arrayExpr: `(${buildChainedArrayExpr(elem)}).flatMap(${elem.flatMapClient.params} => ${elem.flatMapClient.body})`,
|
|
111
|
+
keyFn: elem.flatMapClient.keyed ? '(__bfD, __bfI) => String(__bfD.k ?? __bfI)' : 'null',
|
|
112
|
+
paramHead: '__bfD',
|
|
113
|
+
paramUnwrap: '',
|
|
114
|
+
indexParam: '__idx',
|
|
115
|
+
mapPreambleWrapped: '',
|
|
116
|
+
template: '',
|
|
117
|
+
reactiveEffects: null,
|
|
118
|
+
childRefs: [],
|
|
119
|
+
bodyIsMultiRoot: false,
|
|
120
|
+
anchored: false,
|
|
121
|
+
anchorKeyExpr: '__idx',
|
|
122
|
+
flatMapLeafItem: true,
|
|
123
|
+
// flatMap loops carry no `MapCallbackPreamble` (jsx-to-ir.ts drops it
|
|
124
|
+
// for flatMapCallback), so there is nothing to patch here.
|
|
125
|
+
preambleRegions: [],
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
return {
|
|
95
130
|
kind: 'plain',
|
|
96
131
|
rowConstruction: 'string-template',
|
|
@@ -116,6 +151,7 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
116
151
|
skeletonPaths: elem.skeletonPaths,
|
|
117
152
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
118
153
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
154
|
+
preambleRegions: buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings),
|
|
119
155
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
120
156
|
anchored: elem.bodyIsItemConditional ?? false,
|
|
121
157
|
// Fall back to the iteration index when the loop has no key. A whole-item
|
|
@@ -66,8 +66,19 @@ export interface KeyedItemLookup {
|
|
|
66
66
|
* decision.
|
|
67
67
|
*/
|
|
68
68
|
keyWithItem: string
|
|
69
|
-
/** Optional preamble line — emitted before the handler call. */
|
|
69
|
+
/** Optional preamble line — emitted before the handler call, inside the item guard. */
|
|
70
70
|
mapPreamble: string | null
|
|
71
|
+
/**
|
|
72
|
+
* `const`/`let`/function names `mapPreamble` declares (from
|
|
73
|
+
* `MapCallbackPreamble.declaredNames`). The stringifier only splices
|
|
74
|
+
* `mapPreamble` for an event whose handler text actually references one of
|
|
75
|
+
* these — a preamble that only feeds unused local state (e.g. a `cells`
|
|
76
|
+
* array builder never read by the click handler) is dead weight the
|
|
77
|
+
* delegated handler shouldn't pay for, and emitting it unconditionally is
|
|
78
|
+
* what let BUG-3's stale-context leaf render go unnoticed. Empty when
|
|
79
|
+
* `mapPreamble` is `null`.
|
|
80
|
+
*/
|
|
81
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
71
82
|
/** True when `paramBindings` is non-empty — drives TDZ-safe lookup shape. */
|
|
72
83
|
hasBindings: boolean
|
|
73
84
|
}
|
|
@@ -77,6 +88,8 @@ export interface DynamicIndexItemLookup {
|
|
|
77
88
|
arrayExpr: string
|
|
78
89
|
param: string
|
|
79
90
|
mapPreamble: string | null
|
|
91
|
+
/** See `KeyedItemLookup.mapPreambleDeclaredNames`. */
|
|
92
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
80
93
|
hasBindings: boolean
|
|
81
94
|
/** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
|
|
82
95
|
indexParam: string | null
|
|
@@ -87,6 +100,8 @@ export interface StaticIndexItemLookup {
|
|
|
87
100
|
arrayExpr: string
|
|
88
101
|
param: string
|
|
89
102
|
mapPreamble: string | null
|
|
103
|
+
/** See `KeyedItemLookup.mapPreambleDeclaredNames`. */
|
|
104
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
90
105
|
/** Loop index param name — see `KeyedItemLookup.indexParam` (#2189). */
|
|
91
106
|
indexParam: string | null
|
|
92
107
|
/**
|
|
@@ -78,6 +78,19 @@ interface DynamicLoopCommon extends LoopPlanCommon {
|
|
|
78
78
|
paramUnwrap: string
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* A resolved preamble-patched region (#2389), ready to emit: `valueExpr` is
|
|
83
|
+
* already wrapped with the loop-param accessor and, for a `joinArrayChild`
|
|
84
|
+
* source, already carries the array-join ternary — the exact value
|
|
85
|
+
* `irToHtmlTemplate` computes for the same node in the row template, so a
|
|
86
|
+
* same-key update's re-patch stays byte-identical to what a fresh mount
|
|
87
|
+
* would render.
|
|
88
|
+
*/
|
|
89
|
+
export interface PreambleRegionPlan {
|
|
90
|
+
slotId: string
|
|
91
|
+
valueExpr: string
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
/** Per-item ref callback resolved against a child slot for emission (#1244). */
|
|
82
95
|
export interface LoopChildRefBinding {
|
|
83
96
|
/** bf slot ID of the target element (root or descendant of the body). */
|
|
@@ -146,6 +159,21 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
146
159
|
* Empty when the loop has no key (only meaningful when `anchored`).
|
|
147
160
|
*/
|
|
148
161
|
anchorKeyExpr: string
|
|
162
|
+
/**
|
|
163
|
+
* flatMap descriptor mode (`LoopCore.flatMapClient`): `arrayExpr` already
|
|
164
|
+
* includes the `.flatMap(params => descriptorBody)` projection — the
|
|
165
|
+
* runtime reconciles the FLATTENED `({ k, h })` leaves — and `keyFn` keys
|
|
166
|
+
* on `d.k` (index fallback). The stringifier emits the build-or-patch
|
|
167
|
+
* renderItem (`patchLeaf`) instead of the template-clone shape; `template`
|
|
168
|
+
* is empty and `mapPreambleWrapped` unused in this mode.
|
|
169
|
+
*/
|
|
170
|
+
flatMapLeafItem?: boolean
|
|
171
|
+
/**
|
|
172
|
+
* Preamble-patched regions (#2389) — non-empty forces the multi-line
|
|
173
|
+
* renderItem layout (mirrors `childRefs`) so the region-patch effects have
|
|
174
|
+
* `__el` to query against.
|
|
175
|
+
*/
|
|
176
|
+
preambleRegions: readonly PreambleRegionPlan[]
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/**
|
|
@@ -20,7 +20,8 @@ import { irChildrenToJsExpr } from '../html-template.ts'
|
|
|
20
20
|
import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
21
21
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
22
22
|
import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
23
|
-
import type { LoopChildRefBinding } from './plan/loop.ts'
|
|
23
|
+
import type { LoopChildRefBinding, PreambleRegionPlan } from './plan/loop.ts'
|
|
24
|
+
import type { PreambleRegionSource } from '../../types.ts'
|
|
24
25
|
import {
|
|
25
26
|
extractFreeIdentifiersFromText,
|
|
26
27
|
extractFreeIdentifiersFromStatementText,
|
|
@@ -237,6 +238,30 @@ export function destructureLoopParam(
|
|
|
237
238
|
return { head: param, unwrap: '' }
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Resolve `IRLoop.preambleRegions` (#2389) into ready-to-emit
|
|
243
|
+
* `PreambleRegionPlan`s: wrap each source expression with the loop-param
|
|
244
|
+
* accessor and, for a `joinArrayChild` source, apply the same array-join
|
|
245
|
+
* ternary `irToHtmlTemplate` uses for the identical node in the row
|
|
246
|
+
* template — so the region-patch effect's re-render matches what a fresh
|
|
247
|
+
* mount would have produced. Shared by the top-level (`build-loop.ts`) and
|
|
248
|
+
* branch (`build-branch-loop.ts`) plain-loop-plan builders.
|
|
249
|
+
*/
|
|
250
|
+
export function buildPreambleRegionPlans(
|
|
251
|
+
regions: readonly PreambleRegionSource[] | undefined,
|
|
252
|
+
loopParam: string,
|
|
253
|
+
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
254
|
+
): readonly PreambleRegionPlan[] {
|
|
255
|
+
if (!regions || regions.length === 0) return []
|
|
256
|
+
return regions.map((r) => {
|
|
257
|
+
const wrapped = wrapLoopParamAsAccessor(r.expr, loopParam, loopParamBindings)
|
|
258
|
+
const valueExpr = r.joinArrayChild
|
|
259
|
+
? `Array.isArray(${wrapped}) ? ${wrapped}.join('') : (${wrapped} ?? '')`
|
|
260
|
+
: `escapeText(${wrapped})`
|
|
261
|
+
return { slotId: r.slotId, valueExpr }
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
240
265
|
/**
|
|
241
266
|
* Build a props object expression string from component prop definitions.
|
|
242
267
|
* Shared by emitComponentLoopReconciliation and emitCompositeElementReconciliation.
|
|
@@ -14,7 +14,7 @@ import { stringifyCompositeLoop } from './composite-loop.ts'
|
|
|
14
14
|
import { stringifyEventDelegation } from './event-delegation.ts'
|
|
15
15
|
import { stringifyReactiveEffects } from './reactive-effects.ts'
|
|
16
16
|
import { emitTemplateCloneInline, emitLoopItemElementSetup } from './template-parse.ts'
|
|
17
|
-
import { emitLoopChildRefs } from './loop.ts'
|
|
17
|
+
import { emitLoopChildRefs, emitPreambleRegionEffects } from './loop.ts'
|
|
18
18
|
import type {
|
|
19
19
|
BranchLoopPlan,
|
|
20
20
|
BranchPlainLoopPlan,
|
|
@@ -58,6 +58,7 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
58
58
|
childRefs,
|
|
59
59
|
bodyIsMultiRoot,
|
|
60
60
|
profileLoopId,
|
|
61
|
+
preambleRegions,
|
|
61
62
|
} = plan
|
|
62
63
|
|
|
63
64
|
const loopBfId = profileLoopId ? `, ${JSON.stringify(profileLoopId)}` : ''
|
|
@@ -69,9 +70,29 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
69
70
|
// both the effect and its dependency subscriptions (observation O-2).
|
|
70
71
|
lines.push(` __disposers.push(createDisposableEffect(() => {`)
|
|
71
72
|
|
|
73
|
+
// flatMap descriptor mode — build-or-patch renderItem, mirrors the
|
|
74
|
+
// top-level shape in stringify/loop.ts (see comment there).
|
|
75
|
+
if (plan.flatMapLeafItem) {
|
|
76
|
+
lines.push(` if (${containerVar}) mapArray(() => ${arrayExpr}, ${containerVar}, ${keyFn}, (__bfD, ${indexParam}, __existing) => {`)
|
|
77
|
+
lines.push(` let __el = __existing`)
|
|
78
|
+
lines.push(` if (!__el) { const __tpl = document.createElement('template'); __tpl.innerHTML = __bfD().h; __el = __tpl.content.firstElementChild }`)
|
|
79
|
+
lines.push(` let __last = __existing ? undefined : __bfD().h`)
|
|
80
|
+
lines.push(` createEffect(() => {`)
|
|
81
|
+
lines.push(` const __html = __bfD().h`)
|
|
82
|
+
lines.push(` if (__last === undefined) { __last = __html; return }`)
|
|
83
|
+
lines.push(` if (__html !== __last) { __last = __html; patchLeaf(__el, __html) }`)
|
|
84
|
+
lines.push(` })`)
|
|
85
|
+
lines.push(` return __el`)
|
|
86
|
+
lines.push(` }, '${markerId}'${loopBfId})`)
|
|
87
|
+
lines.push(` }))`)
|
|
88
|
+
stringifyEventDelegation(lines, eventDelegation)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
// Non-empty `childRefs` need `__el` as a handle inside the factory body,
|
|
73
|
-
// so force the multi-line layout (#1244)
|
|
74
|
-
|
|
93
|
+
// so force the multi-line layout (#1244) — preamble-patched regions
|
|
94
|
+
// (#2389) need the same handle for their own effect.
|
|
95
|
+
if (reactiveEffects === null && !bodyIsMultiRoot && childRefs.length === 0 && preambleRegions.length === 0) {
|
|
75
96
|
// Simple case: single-line renderItem (single root, no reactive effects).
|
|
76
97
|
const cloneExpr = emitTemplateCloneInline(template)
|
|
77
98
|
if (mapPreambleWrapped) {
|
|
@@ -98,6 +119,7 @@ function emitPlain(lines: string[], plan: BranchPlainLoopPlan): void {
|
|
|
98
119
|
stringifyReactiveEffects(lines, reactiveEffects, { indent: ' ', elVar: '__el', bodyIsMultiRoot })
|
|
99
120
|
}
|
|
100
121
|
emitLoopChildRefs(lines, childRefs, { indent: ' ', elVar: '__el', bodyIsMultiRoot })
|
|
122
|
+
emitPreambleRegionEffects(lines, preambleRegions, mapPreambleWrapped, { indent: ' ', elVar: '__el' })
|
|
101
123
|
lines.push(` return __el`)
|
|
102
124
|
lines.push(` }, '${markerId}'${loopBfId})`)
|
|
103
125
|
}
|