@barefootjs/jsx 0.26.2 → 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/adapters/interface.d.ts +29 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/jsx-adapter.d.ts +9 -0
- package/dist/adapters/jsx-adapter.d.ts.map +1 -1
- package/dist/adapters/loop-bound-names.d.ts.map +1 -1
- package/dist/adapters/parsed-expr-emitter.d.ts +0 -10
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/adapters/test-adapter.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +11 -1
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts +40 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.js +1086 -269
- 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 +14 -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-component-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-composite-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-inner-loop.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 +39 -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 +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/control-flow.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +67 -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/plan/build-static-array-child-init.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 +29 -4
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/loop-destructure.d.ts.map +1 -1
- package/dist/strip-types.d.ts +18 -0
- package/dist/strip-types.d.ts.map +1 -1
- package/dist/types.d.ts +143 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +277 -33
- package/src/__tests__/client-js-generation.test.ts +18 -5
- package/src/__tests__/compiler-runtime-contract.test.ts +4 -4
- package/src/__tests__/compiler-stress-1244.test.ts +12 -1
- package/src/__tests__/delegated-handler-preamble.test.ts +153 -0
- package/src/__tests__/event-delegation-scope-2367.test.ts +108 -0
- package/src/__tests__/flatmap-segments.test.ts +262 -0
- package/src/__tests__/map-arbitrary-body.test.ts +226 -0
- package/src/__tests__/map-body-no-silent-divergence.test.ts +405 -0
- package/src/__tests__/map-multi-return-body.test.ts +172 -0
- package/src/__tests__/preamble-region-patch.test.ts +150 -0
- package/src/__tests__/static-loop-csr-materialize.test.ts +3 -2
- package/src/__tests__/unsupported-expression.test.ts +20 -2
- package/src/adapters/interface.ts +40 -0
- package/src/adapters/jsx-adapter.ts +10 -0
- package/src/adapters/loop-bound-names.ts +6 -2
- package/src/adapters/parsed-expr-emitter.ts +5 -10
- package/src/adapters/test-adapter.ts +10 -0
- package/src/analyzer-context.ts +35 -1
- package/src/analyzer.ts +162 -24
- package/src/compiler.ts +2 -2
- package/src/expression-parser.ts +27 -19
- package/src/ir-to-client-js/build-references.ts +19 -2
- package/src/ir-to-client-js/collect-elements.ts +69 -13
- package/src/ir-to-client-js/control-flow/plan/branch-loop.ts +14 -1
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +18 -5
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +4 -0
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +15 -2
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +32 -3
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +26 -3
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +53 -2
- 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 +40 -0
- package/src/ir-to-client-js/control-flow/shared.ts +28 -2
- 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 +76 -21
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +65 -1
- package/src/ir-to-client-js/control-flow.ts +11 -0
- package/src/ir-to-client-js/html-template.ts +244 -39
- package/src/ir-to-client-js/imports.ts +1 -1
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +22 -10
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/types.ts +28 -3
- package/src/jsx-to-ir.ts +1059 -167
- package/src/loop-destructure.ts +9 -5
- package/src/rich-type-refusal.ts +6 -2
- package/src/strip-types.ts +47 -0
- package/src/types.ts +159 -35
|
@@ -60,6 +60,10 @@ export function buildComponentLoopPlan(elem: TopLevelLoop, profileComponentName?
|
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
62
|
kind: 'component',
|
|
63
|
+
// Rows are createComponent-driven: a lowered HTML-string leaf passed as a
|
|
64
|
+
// prop would diverge from SSR (which passes real JSX elements). The plan
|
|
65
|
+
// dispatcher refuses a JSX-bearing preamble on this variant.
|
|
66
|
+
rowConstruction: 'dom-ops',
|
|
63
67
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
64
68
|
markerId: elem.markerId,
|
|
65
69
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from '../shared.ts'
|
|
24
24
|
import { buildReactiveEffectsPlan } from './build-reactive-effects.ts'
|
|
25
25
|
import { buildInnerLoopsPlan } from './build-inner-loop.ts'
|
|
26
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
26
27
|
import type { CompositeLoopPlan } from './types.ts'
|
|
27
28
|
|
|
28
29
|
/** @internal — prefer `buildLoopPlan`. */
|
|
@@ -36,6 +37,7 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
36
37
|
|
|
37
38
|
return {
|
|
38
39
|
kind: 'composite',
|
|
40
|
+
rowConstruction: 'string-template',
|
|
39
41
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
40
42
|
markerId: elem.markerId,
|
|
41
43
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
@@ -43,7 +45,12 @@ export function buildTopLevelCompositePlan(elem: TopLevelLoop, profileComponentN
|
|
|
43
45
|
paramHead,
|
|
44
46
|
paramUnwrap,
|
|
45
47
|
indexParam: elem.index || '__idx',
|
|
46
|
-
mapPreambleWrapped: elem.
|
|
48
|
+
mapPreambleWrapped: elem.preamble
|
|
49
|
+
? renderPreamble(elem.preamble, {
|
|
50
|
+
transformJs: wrap,
|
|
51
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
52
|
+
})
|
|
53
|
+
: '',
|
|
47
54
|
template: elem.template,
|
|
48
55
|
outerComps: filterCondCompsOut(outerCompsByDepth, elem.bindings.conditionals),
|
|
49
56
|
outerEvents: elem.bindings.events.filter(ev => ev.nestedLoops.length === 0),
|
|
@@ -87,6 +94,7 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
87
94
|
|
|
88
95
|
return {
|
|
89
96
|
kind: 'composite',
|
|
97
|
+
rowConstruction: 'string-template',
|
|
90
98
|
containerVar: `__loop_${cv}`,
|
|
91
99
|
markerId: loop.markerId,
|
|
92
100
|
// Chain `.filter()` / `.toSorted()` onto the source array so the mapArray
|
|
@@ -97,7 +105,12 @@ export function buildBranchCompositePlan(loop: BranchLoop, cv: string, profileCo
|
|
|
97
105
|
paramHead,
|
|
98
106
|
paramUnwrap,
|
|
99
107
|
indexParam: loop.index || '__idx',
|
|
100
|
-
mapPreambleWrapped: loop.
|
|
108
|
+
mapPreambleWrapped: loop.preamble
|
|
109
|
+
? renderPreamble(loop.preamble, {
|
|
110
|
+
transformJs: wrap,
|
|
111
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: loop.param, bindings: loop.paramBindings }], undefined, true),
|
|
112
|
+
})
|
|
113
|
+
: '',
|
|
101
114
|
template: loop.template,
|
|
102
115
|
outerComps: filterCondCompsOut(outerCompsByDepth, loop.bindings.conditionals),
|
|
103
116
|
outerEvents: childEvents.filter(ev => ev.nestedLoops.length === 0),
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { TopLevelLoop, BranchLoop, LoopChildEvent } from '../../types.ts'
|
|
13
13
|
import { buildChainedArrayExpr, varSlotId, substituteLoopBindings } from '../../utils.ts'
|
|
14
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
14
15
|
import type {
|
|
15
16
|
EventDelegationPlan,
|
|
16
17
|
ItemLookup,
|
|
@@ -35,7 +36,18 @@ export function buildDynamicLoopDelegationPlan(
|
|
|
35
36
|
paramBindings: elem.paramBindings,
|
|
36
37
|
key: elem.key,
|
|
37
38
|
index: elem.index,
|
|
38
|
-
|
|
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.
|
|
45
|
+
mapPreamble: elem.preamble
|
|
46
|
+
? renderPreamble(elem.preamble, {
|
|
47
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
48
|
+
})
|
|
49
|
+
: null,
|
|
50
|
+
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
39
51
|
}),
|
|
40
52
|
}
|
|
41
53
|
}
|
|
@@ -60,7 +72,14 @@ export function buildBranchLoopDelegationPlan(
|
|
|
60
72
|
paramBindings: loop.paramBindings,
|
|
61
73
|
key: loop.key,
|
|
62
74
|
index: loop.index,
|
|
63
|
-
|
|
75
|
+
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
76
|
+
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
77
|
+
mapPreamble: loop.preamble
|
|
78
|
+
? renderPreamble(loop.preamble, {
|
|
79
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
80
|
+
})
|
|
81
|
+
: null,
|
|
82
|
+
mapPreambleDeclaredNames: loop.preamble?.declaredNames ?? [],
|
|
64
83
|
}),
|
|
65
84
|
}
|
|
66
85
|
}
|
|
@@ -86,7 +105,14 @@ export function buildStaticArrayDelegationPlan(
|
|
|
86
105
|
// array too (#1434).
|
|
87
106
|
arrayExpr: buildChainedArrayExpr(elem),
|
|
88
107
|
param: elem.param,
|
|
89
|
-
|
|
108
|
+
// See note in `buildDynamicLoopDelegationPlan` above (BUG-3): no
|
|
109
|
+
// loopParams spec — leaf refs must stay in plain-object form here.
|
|
110
|
+
mapPreamble: elem.preamble
|
|
111
|
+
? renderPreamble(elem.preamble, {
|
|
112
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
113
|
+
})
|
|
114
|
+
: null,
|
|
115
|
+
mapPreambleDeclaredNames: elem.preamble?.declaredNames ?? [],
|
|
90
116
|
offset: elem.offset ?? null,
|
|
91
117
|
indexParam: elem.index ?? null,
|
|
92
118
|
},
|
|
@@ -104,6 +130,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
104
130
|
key: string | null
|
|
105
131
|
index: string | null
|
|
106
132
|
mapPreamble: string | null
|
|
133
|
+
mapPreambleDeclaredNames: readonly string[]
|
|
107
134
|
}): ItemLookup {
|
|
108
135
|
const hasBindings = (args.paramBindings?.length ?? 0) > 0
|
|
109
136
|
if (args.key !== null) {
|
|
@@ -119,6 +146,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
119
146
|
paramBindings: args.paramBindings,
|
|
120
147
|
keyWithItem,
|
|
121
148
|
mapPreamble: args.mapPreamble,
|
|
149
|
+
mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
|
|
122
150
|
hasBindings,
|
|
123
151
|
indexParam: args.index,
|
|
124
152
|
}
|
|
@@ -128,6 +156,7 @@ function buildKeyedOrIndexLookup(args: {
|
|
|
128
156
|
arrayExpr: args.array,
|
|
129
157
|
param: args.param,
|
|
130
158
|
mapPreamble: args.mapPreamble,
|
|
159
|
+
mapPreambleDeclaredNames: args.mapPreambleDeclaredNames,
|
|
131
160
|
hasBindings,
|
|
132
161
|
indexParam: args.index,
|
|
133
162
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
attrValueToString,
|
|
30
30
|
} from '../../utils.ts'
|
|
31
31
|
import { buildChildRefBindings, buildStaticChildRefBindings } from '../shared.ts'
|
|
32
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Mirror of the helper in `build-loop-child-arm.ts` — kept local to avoid
|
|
@@ -115,7 +116,7 @@ export function buildInnerLoopsPlan(args: BuildInnerLoopsArgs): InnerLoopsPlan {
|
|
|
115
116
|
const useReactive = refsParent && !!inner.template
|
|
116
117
|
|
|
117
118
|
const emit: InnerLoopReactiveEmit | InnerLoopStaticEmit = useReactive
|
|
118
|
-
? buildReactiveEmit(inner, level, wrapOuter, uidSuffix)
|
|
119
|
+
? buildReactiveEmit(inner, level, wrapOuter, uidSuffix, outerLoopParam, outerLoopParamBindings)
|
|
119
120
|
: buildStaticEmit(inner, level, uidSuffix)
|
|
120
121
|
|
|
121
122
|
const arrayExpr = useReactive ? wrapOuter(inner.array) : inner.array
|
|
@@ -155,6 +156,8 @@ function buildReactiveEmit(
|
|
|
155
156
|
level: DepthLevel,
|
|
156
157
|
wrapOuter: (expr: string) => string,
|
|
157
158
|
uidSuffix: string,
|
|
159
|
+
outerLoopParam?: string,
|
|
160
|
+
outerLoopParamBindings?: readonly LoopParamBinding[],
|
|
158
161
|
): InnerLoopReactiveEmit {
|
|
159
162
|
const wrapInner = (expr: string) => wrapLoopParamAsAccessor(expr, inner.param, inner.paramBindings)
|
|
160
163
|
const { head: paramHead, unwrap: paramUnwrap } = destructureLoopParam(inner.param, inner.paramBindings)
|
|
@@ -229,7 +232,20 @@ function buildReactiveEmit(
|
|
|
229
232
|
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, paramHead, level.comps, level.events)
|
|
230
233
|
if (indexAlias) preludeStatements.push(indexAlias)
|
|
231
234
|
if (paramUnwrap) preludeStatements.push(paramUnwrap)
|
|
232
|
-
if (inner.
|
|
235
|
+
if (inner.preamble) {
|
|
236
|
+
// Leaf JSX renders under both param contexts, mirroring the
|
|
237
|
+
// wrapInner(wrapOuter(...)) applied to the js text.
|
|
238
|
+
const leafLoopParams = outerLoopParam
|
|
239
|
+
? [
|
|
240
|
+
{ param: outerLoopParam, bindings: outerLoopParamBindings },
|
|
241
|
+
{ param: inner.param, bindings: inner.paramBindings },
|
|
242
|
+
]
|
|
243
|
+
: [{ param: inner.param, bindings: inner.paramBindings }]
|
|
244
|
+
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
245
|
+
transformJs: (t) => wrapInner(wrapOuter(t)),
|
|
246
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, leafLoopParams, undefined, true),
|
|
247
|
+
}))
|
|
248
|
+
}
|
|
233
249
|
|
|
234
250
|
const childRefs = buildChildRefBindings(inner.bindings.refs, inner.param, inner.paramBindings)
|
|
235
251
|
|
|
@@ -265,7 +281,14 @@ function buildStaticEmit(inner: NestedLoop, level: DepthLevel, uidSuffix: string
|
|
|
265
281
|
const preludeStatements: string[] = []
|
|
266
282
|
const indexAlias = nestedLoopIndexAlias(inner, `__innerIdx${uidSuffix}`, inner.param, level.comps, level.events)
|
|
267
283
|
if (indexAlias) preludeStatements.push(indexAlias)
|
|
268
|
-
|
|
284
|
+
// Static `forEach` receives the literal item, so neither the js text nor a
|
|
285
|
+
// leaf's param reads get accessor-wrapped (mirrors the verbatim emission
|
|
286
|
+
// of the js segments above).
|
|
287
|
+
if (inner.preamble) {
|
|
288
|
+
preludeStatements.push(renderPreamble(inner.preamble, {
|
|
289
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, undefined, undefined, true),
|
|
290
|
+
}))
|
|
291
|
+
}
|
|
269
292
|
return {
|
|
270
293
|
mode: 'static',
|
|
271
294
|
rawKey: inner.key ?? null,
|
|
@@ -35,10 +35,12 @@ 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'
|
|
41
42
|
import { buildTopLevelCompositePlan } from './build-composite-loop.ts'
|
|
43
|
+
import { renderPreamble, irToHtmlTemplate } from '../../html-template.ts'
|
|
42
44
|
import type {
|
|
43
45
|
LoopPlan,
|
|
44
46
|
PlainLoopPlan,
|
|
@@ -90,8 +92,43 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
90
92
|
|| elem.bindings.reactiveTexts.length > 0
|
|
91
93
|
|| elem.bindings.conditionals.length > 0
|
|
92
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
|
+
|
|
93
129
|
return {
|
|
94
130
|
kind: 'plain',
|
|
131
|
+
rowConstruction: 'string-template',
|
|
95
132
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
96
133
|
markerId: elem.markerId,
|
|
97
134
|
profileLoopId: profileComponentName ? `${profileComponentName}#binding:${elem.slotId}` : undefined,
|
|
@@ -100,12 +137,21 @@ export function buildPlainLoopPlan(elem: TopLevelLoop, profileComponentName?: st
|
|
|
100
137
|
paramHead,
|
|
101
138
|
paramUnwrap,
|
|
102
139
|
indexParam: elem.index || '__idx',
|
|
103
|
-
|
|
140
|
+
// Stage 3 / D4 — js segments get the loop-param accessor wrap; jsx leaves
|
|
141
|
+
// render as HTML-string templates under this loop's param context so a
|
|
142
|
+
// leaf that reads the item (`r`) becomes `r()`.
|
|
143
|
+
mapPreambleWrapped: elem.preamble
|
|
144
|
+
? renderPreamble(elem.preamble, {
|
|
145
|
+
transformJs: wrap,
|
|
146
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
147
|
+
})
|
|
148
|
+
: '',
|
|
104
149
|
template: elem.template,
|
|
105
150
|
skeletonTemplate: elem.skeletonTemplate,
|
|
106
151
|
skeletonPaths: elem.skeletonPaths,
|
|
107
152
|
reactiveEffects: hasReactive ? buildLoopReactiveEffectsPlan(elem, profileComponentName) : null,
|
|
108
153
|
childRefs: buildChildRefBindings(elem.bindings.refs, elem.param, elem.paramBindings),
|
|
154
|
+
preambleRegions: buildPreambleRegionPlans(elem.preambleRegions, elem.param, elem.paramBindings),
|
|
109
155
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
110
156
|
anchored: elem.bodyIsItemConditional ?? false,
|
|
111
157
|
// Fall back to the iteration index when the loop has no key. A whole-item
|
|
@@ -139,6 +185,7 @@ export function buildStaticLoopPlan(elem: TopLevelLoop, unsafeLocalNames: Set<st
|
|
|
139
185
|
|
|
140
186
|
return {
|
|
141
187
|
kind: 'static',
|
|
188
|
+
rowConstruction: 'string-template',
|
|
142
189
|
containerVar: `_${varSlotId(elem.slotId)}`,
|
|
143
190
|
arrayExpr: elem.array,
|
|
144
191
|
param: elem.param,
|
|
@@ -188,7 +235,11 @@ function buildStaticLoopMaterialize(
|
|
|
188
235
|
if (!setIntersects(elem.arrayFreeIdentifiers, unsafeLocalNames)) return null
|
|
189
236
|
return {
|
|
190
237
|
itemTemplate: elem.staticItemTemplate,
|
|
191
|
-
mapPreamble: elem.
|
|
238
|
+
mapPreamble: elem.preamble
|
|
239
|
+
? renderPreamble(elem.preamble, {
|
|
240
|
+
renderLeaf: (ir) => irToHtmlTemplate(ir, undefined, 1, [{ param: elem.param, bindings: elem.paramBindings }], undefined, true),
|
|
241
|
+
})
|
|
242
|
+
: '',
|
|
192
243
|
bodyIsMultiRoot: elem.bodyIsMultiRoot ?? false,
|
|
193
244
|
}
|
|
194
245
|
}
|
|
@@ -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
|
/**
|
|
@@ -45,6 +45,18 @@ interface LoopPlanCommon {
|
|
|
45
45
|
* — initial mount, SSR hydration, and same-key remount after unmount.
|
|
46
46
|
*/
|
|
47
47
|
childRefs: readonly LoopChildRefBinding[]
|
|
48
|
+
/**
|
|
49
|
+
* How this variant constructs each row's DOM (Stage 3 root cure,
|
|
50
|
+
* spec/callback-fidelity.md). `'string-template'` = rows are built from
|
|
51
|
+
* interpolated innerHTML template literals, so a `.map()` preamble carrying
|
|
52
|
+
* lowered JSX leaves can be spliced via `renderPreamble()`. `'dom-ops'` =
|
|
53
|
+
* rows are built imperatively (createElement/reconcile paths) and CANNOT
|
|
54
|
+
* host a string-lowered preamble — the plan dispatcher refuses a JSX-bearing
|
|
55
|
+
* preamble on such a variant instead of letting it drop silently. Required
|
|
56
|
+
* on every variant (like `childRefs`) so a new loop shape must declare its
|
|
57
|
+
* capability — the compile error is the point.
|
|
58
|
+
*/
|
|
59
|
+
rowConstruction: 'string-template' | 'dom-ops'
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/** Fields shared by every dynamic (`mapArray`-driven) loop variant. */
|
|
@@ -66,6 +78,19 @@ interface DynamicLoopCommon extends LoopPlanCommon {
|
|
|
66
78
|
paramUnwrap: string
|
|
67
79
|
}
|
|
68
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
|
+
|
|
69
94
|
/** Per-item ref callback resolved against a child slot for emission (#1244). */
|
|
70
95
|
export interface LoopChildRefBinding {
|
|
71
96
|
/** bf slot ID of the target element (root or descendant of the body). */
|
|
@@ -134,6 +159,21 @@ interface PlainLoopVariant extends DynamicLoopCommon {
|
|
|
134
159
|
* Empty when the loop has no key (only meaningful when `anchored`).
|
|
135
160
|
*/
|
|
136
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[]
|
|
137
177
|
}
|
|
138
178
|
|
|
139
179
|
/**
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
import type { LoopChildEvent, LoopChildRef, TopLevelLoop, NestedLoop, CollectedLoop } from '../types.ts'
|
|
16
16
|
import type { IRLoopChildComponent, LoopParamBinding } from '../../types.ts'
|
|
17
|
+
import { preambleAnalysisText } from '../../types.ts'
|
|
17
18
|
import { quotePropName, wrapLoopParamAsAccessor, irChildrenFreeIds, attrValueToString } from '../utils.ts'
|
|
18
19
|
import { irChildrenToJsExpr } from '../html-template.ts'
|
|
19
20
|
import { emitListenerBlock } from './stringify/event-listener.ts'
|
|
20
21
|
import { nameForRegistryRef } from '../component-scope.ts'
|
|
21
22
|
import { BF_SCOPE, BF_HOST, BF_AT } from '@barefootjs/shared'
|
|
22
|
-
import type { LoopChildRefBinding } from './plan/loop.ts'
|
|
23
|
+
import type { LoopChildRefBinding, PreambleRegionPlan } from './plan/loop.ts'
|
|
24
|
+
import type { PreambleRegionSource } from '../../types.ts'
|
|
23
25
|
import {
|
|
24
26
|
extractFreeIdentifiersFromText,
|
|
25
27
|
extractFreeIdentifiersFromStatementText,
|
|
@@ -83,7 +85,7 @@ export function nestedLoopReferencesIndex(
|
|
|
83
85
|
for (const a of inner.bindings.reactiveAttrs) {
|
|
84
86
|
if (exprRefs(a.expression, a.freeIdentifiers)) return true
|
|
85
87
|
}
|
|
86
|
-
if (inner.
|
|
88
|
+
if (inner.preamble && extractFreeIdentifiersFromStatementText(preambleAnalysisText(inner.preamble)).has(index)) return true
|
|
87
89
|
if (inner.template && extractFreeIdentifiersFromTemplateText(inner.template).has(index)) return true
|
|
88
90
|
for (const ev of events) {
|
|
89
91
|
if (exprRefs(ev.handler)) return true
|
|
@@ -236,6 +238,30 @@ export function destructureLoopParam(
|
|
|
236
238
|
return { head: param, unwrap: '' }
|
|
237
239
|
}
|
|
238
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
|
+
|
|
239
265
|
/**
|
|
240
266
|
* Build a props object expression string from component prop definitions.
|
|
241
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
|
}
|