@barefootjs/jsx 0.24.1 → 0.26.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/dangerous-inner-html.d.ts +52 -24
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.js +473 -165
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
- 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/loop-child-arm.d.ts +11 -5
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts +24 -2
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/types.d.ts +13 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +31 -10
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +29 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
- package/src/__tests__/nested-loop-conditional.test.ts +133 -0
- package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
- package/src/__tests__/reactive-factory-cross-file.test.ts +833 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
- package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +607 -142
- package/src/errors.ts +4 -0
- package/src/ir-to-client-js/collect-elements.ts +28 -2
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
- package/src/ir-to-client-js/reactivity.ts +61 -7
- package/src/ir-to-client-js/types.ts +13 -0
- package/src/rich-type-refusal.ts +3 -2
- package/src/to-locale-date-lowering.ts +50 -13
- package/src/types.ts +30 -5
package/src/errors.ts
CHANGED
|
@@ -90,6 +90,7 @@ export const ErrorCodes = {
|
|
|
90
90
|
REACTIVE_FACTORY_RENAME_UNSUPPORTED: 'BF111',
|
|
91
91
|
REACTIVE_FACTORY_MODULE_CAPTURE: 'BF112',
|
|
92
92
|
REACTIVE_FACTORY_IMPORT_COLLISION: 'BF113',
|
|
93
|
+
REACTIVE_FACTORY_PARAM_SHADOWED: 'BF114',
|
|
93
94
|
} as const
|
|
94
95
|
|
|
95
96
|
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
|
@@ -184,6 +185,9 @@ const errorMessages: Record<ErrorCode, string> = {
|
|
|
184
185
|
'Inlining an imported reactive factory requires re-importing one of its helper ' +
|
|
185
186
|
'imports into this file, but that name is already bound here to something else. ' +
|
|
186
187
|
"Rename the conflicting binding in this file, or alias the import in the factory's own file.",
|
|
188
|
+
|
|
189
|
+
[ErrorCodes.REACTIVE_FACTORY_PARAM_SHADOWED]:
|
|
190
|
+
'Reactive factory parameter is shadowed by a nested declaration inside the factory body, so argument substitution at the inline site would be ambiguous. Rename the inner binding so it does not collide with the parameter.',
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
// =============================================================================
|
|
@@ -1246,8 +1246,13 @@ export function collectLoopChildBindings(
|
|
|
1246
1246
|
const bindings = emptyLoopChildBindings()
|
|
1247
1247
|
for (const child of children) {
|
|
1248
1248
|
bindings.events.push(...collectLoopChildEventsWithNesting(child))
|
|
1249
|
-
|
|
1250
|
-
|
|
1249
|
+
// stopAtReactiveConditionals=true (#2347): this function always also
|
|
1250
|
+
// collects nested reactive conditionals below via
|
|
1251
|
+
// `collectLoopChildConditionals`, which gives each its own insert() +
|
|
1252
|
+
// arm-scoped attrs/texts (`LoopChildBranchSummary.reactiveAttrs` /
|
|
1253
|
+
// `.reactiveTexts`) — descending into them here too would double-bind.
|
|
1254
|
+
bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true))
|
|
1255
|
+
bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true))
|
|
1251
1256
|
bindings.refs.push(...collectLoopChildRefs(child))
|
|
1252
1257
|
bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings))
|
|
1253
1258
|
}
|
|
@@ -1342,5 +1347,26 @@ function summarizeLoopChildBranch(
|
|
|
1342
1347
|
innerLoops: inner.length > 0 ? inner : undefined,
|
|
1343
1348
|
conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings),
|
|
1344
1349
|
events: collectConditionalBranchEvents(node),
|
|
1350
|
+
// Loop-param-aware — reuses the flat loop-item collectors scoped to just
|
|
1351
|
+
// this branch's subtree. Both already stop descending into any further
|
|
1352
|
+
// nested reactive conditional (own insert()/arm), so calling them here
|
|
1353
|
+
// on the branch root yields exactly this branch's direct bindings
|
|
1354
|
+
// without re-collecting what a nested arm already owns (#2347).
|
|
1355
|
+
reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
|
|
1356
|
+
// Skip when the branch's ENTIRE content is a single bare `expression`
|
|
1357
|
+
// (no wrapping element) — e.g. a hoisted `renderNode={(n) => <Pill/>}`
|
|
1358
|
+
// callback (#1211/#1213). That value is already fully re-evaluated and
|
|
1359
|
+
// spliced via `__bfSlot` whenever `insert()` (re-)mounts this branch;
|
|
1360
|
+
// an *additional* nested createEffect for the same expression re-calls
|
|
1361
|
+
// it and creates a second, independent live element (a JSX-callback
|
|
1362
|
+
// result isn't idempotent to re-invoke the way a plain signal read is),
|
|
1363
|
+
// and the loop-child arm's `$t()`-based anchor lookup — designed for
|
|
1364
|
+
// text nodes — doesn't cleanly displace an already-mounted Element,
|
|
1365
|
+
// so the second instance lands beside the first instead of replacing
|
|
1366
|
+
// it. A text *nested inside* a static wrapper element in the branch
|
|
1367
|
+
// (the element-descent case) is unaffected and still collected below.
|
|
1368
|
+
reactiveTexts: node.type === 'expression'
|
|
1369
|
+
? []
|
|
1370
|
+
: collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true),
|
|
1345
1371
|
}
|
|
1346
1372
|
}
|
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
LoopChildBranchSummary,
|
|
12
12
|
LoopChildConditional,
|
|
13
13
|
LoopChildEvent,
|
|
14
|
+
LoopChildReactiveAttr,
|
|
15
|
+
LoopChildReactiveText,
|
|
14
16
|
NestedLoop,
|
|
15
17
|
} from '../../types.ts'
|
|
16
18
|
import type {
|
|
@@ -20,9 +22,10 @@ import type {
|
|
|
20
22
|
IRProp,
|
|
21
23
|
LoopParamBinding,
|
|
22
24
|
} from '../../../types.ts'
|
|
23
|
-
import { AttrValueOf } from '../../../types.ts'
|
|
25
|
+
import { AttrValueOf, pickAttrMeta } from '../../../types.ts'
|
|
24
26
|
import { quotePropName, wrapLoopParamAsAccessor, attrValueToString } from '../../utils.ts'
|
|
25
27
|
import { addCondAttrToTemplate, irChildrenToJsExpr } from '../../html-template.ts'
|
|
28
|
+
import type { ReactiveAttrSlot } from './reactive-effects.ts'
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Apply a string-level expression rewriter (loop-param-accessor wrap, prop
|
|
@@ -381,6 +384,56 @@ export function buildLoopChildConditionalsPlan(
|
|
|
381
384
|
return plans
|
|
382
385
|
}
|
|
383
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Group a branch's reactive attrs by child slot (one qsa per slot),
|
|
389
|
+
* pre-wrapping each expression via the supplied loop-param wrap closure.
|
|
390
|
+
* Shared by every arm builder — outer conditional arms and recursively
|
|
391
|
+
* nested ones alike — so an attr binds inside whichever arm directly owns
|
|
392
|
+
* its element, never a stale outer scope (#2347).
|
|
393
|
+
*/
|
|
394
|
+
export function buildArmAttrsPlan(
|
|
395
|
+
attrs: readonly LoopChildReactiveAttr[] | undefined,
|
|
396
|
+
wrap: (expr: string) => string,
|
|
397
|
+
): readonly ReactiveAttrSlot[] {
|
|
398
|
+
if (!attrs || attrs.length === 0) return []
|
|
399
|
+
const bySlot = new Map<string, LoopChildReactiveAttr[]>()
|
|
400
|
+
for (const attr of attrs) {
|
|
401
|
+
let bucket = bySlot.get(attr.childSlotId)
|
|
402
|
+
if (!bucket) {
|
|
403
|
+
bucket = []
|
|
404
|
+
bySlot.set(attr.childSlotId, bucket)
|
|
405
|
+
}
|
|
406
|
+
bucket.push(attr)
|
|
407
|
+
}
|
|
408
|
+
const slots: ReactiveAttrSlot[] = []
|
|
409
|
+
for (const [slotId, slotAttrs] of bySlot) {
|
|
410
|
+
slots.push({
|
|
411
|
+
slotId,
|
|
412
|
+
attrs: slotAttrs.map(attr => ({
|
|
413
|
+
attrName: attr.attrName,
|
|
414
|
+
wrappedExpression: wrap(attr.expression),
|
|
415
|
+
meta: pickAttrMeta(attr),
|
|
416
|
+
})),
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
return slots
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Pre-wrap a branch's reactive text interpolations via the supplied
|
|
424
|
+
* loop-param wrap closure. Shared by every arm builder (#2347).
|
|
425
|
+
*/
|
|
426
|
+
export function buildArmTextsPlan(
|
|
427
|
+
texts: readonly LoopChildReactiveText[] | undefined,
|
|
428
|
+
wrap: (expr: string) => string,
|
|
429
|
+
): readonly import('./loop-child-arm.ts').LoopChildArmText[] {
|
|
430
|
+
if (!texts || texts.length === 0) return []
|
|
431
|
+
return texts.map(text => ({
|
|
432
|
+
slotId: text.slotId,
|
|
433
|
+
wrappedExpression: wrap(text.expression),
|
|
434
|
+
}))
|
|
435
|
+
}
|
|
436
|
+
|
|
384
437
|
interface BuildLoopChildArmArgs {
|
|
385
438
|
branch: LoopChildBranchSummary
|
|
386
439
|
wrap: (expr: string) => string
|
|
@@ -413,6 +466,7 @@ function buildLoopChildArmPlan(args: BuildLoopChildArmArgs): LoopChildArmPlan {
|
|
|
413
466
|
loopParam,
|
|
414
467
|
loopParamBindings,
|
|
415
468
|
}),
|
|
416
|
-
|
|
469
|
+
attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
|
|
470
|
+
texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
|
|
417
471
|
}
|
|
418
472
|
}
|
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Decisions resolved at build time (no longer in the stringifier):
|
|
5
5
|
* 1. Group reactive attrs by `childSlotId` (one qsa() lookup per slot).
|
|
6
|
+
* `attrs` only ever carries outer-scope entries — the collector that
|
|
7
|
+
* produces it already stops descending into reactive conditionals
|
|
8
|
+
* (#2347), so no branch/outer partition is needed here.
|
|
6
9
|
* 2. Wrap every attr / text / condition expression via
|
|
7
10
|
* `wrapLoopParamAsAccessor` so the stringifier never touches the wrap.
|
|
8
|
-
* 3.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* 3. Each conditional arm gets its own reactive attrs / texts directly
|
|
12
|
+
* from `LoopChildBranchSummary.reactiveAttrs` / `.reactiveTexts` —
|
|
13
|
+
* collected per-branch, recursively, so a doubly-nested conditional's
|
|
14
|
+
* arm carries its own bindings instead of an outer scope reaching in.
|
|
12
15
|
* 4. Apply `addCondAttrToTemplate` to the wrapped branch HTML so the
|
|
13
16
|
* stringifier emits a ready-to-interpolate template literal.
|
|
14
17
|
* 5. Recurse into the per-arm sub-plans via `LoopChildArmPlan` —
|
|
15
|
-
* events, child component inits, inner loops, nested conditionals
|
|
16
|
-
* No legacy passthrough remains.
|
|
18
|
+
* events, child component inits, inner loops, nested conditionals,
|
|
19
|
+
* attrs, texts. No legacy passthrough remains.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
import type {
|
|
@@ -28,15 +31,14 @@ import { pickAttrMeta } from '../../../types.ts'
|
|
|
28
31
|
import { wrapLoopParamAsAccessor } from '../../utils.ts'
|
|
29
32
|
import { addCondAttrToTemplate } from '../../html-template.ts'
|
|
30
33
|
import {
|
|
34
|
+
buildArmAttrsPlan,
|
|
35
|
+
buildArmTextsPlan,
|
|
31
36
|
buildBranchChildComponentInitsPlan,
|
|
32
37
|
buildBranchEventBindingsPlan,
|
|
33
38
|
buildBranchInnerLoopsPlan,
|
|
34
39
|
buildLoopChildConditionalsPlan,
|
|
35
40
|
} from './build-loop-child-arm.ts'
|
|
36
|
-
import type {
|
|
37
|
-
LoopChildArmPlan,
|
|
38
|
-
LoopChildArmText,
|
|
39
|
-
} from './loop-child-arm.ts'
|
|
41
|
+
import type { LoopChildArmPlan } from './loop-child-arm.ts'
|
|
40
42
|
import type {
|
|
41
43
|
NestedConditionalPlan,
|
|
42
44
|
ReactiveAttrSlot,
|
|
@@ -84,57 +86,31 @@ export function buildReactiveEffectsPlan(
|
|
|
84
86
|
})
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
// 2.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
cond.whenTrueHtml.includes(`bf:${text.slotId}`) ||
|
|
97
|
-
cond.whenFalseHtml.includes(`bf:${text.slotId}`)
|
|
98
|
-
) {
|
|
99
|
-
textSlotsInConditionals.add(text.slotId)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const outerTexts: ReactiveTextEffect[] = []
|
|
106
|
-
for (const text of texts) {
|
|
107
|
-
if (textSlotsInConditionals.has(text.slotId)) continue
|
|
108
|
-
outerTexts.push({
|
|
109
|
-
slotId: text.slotId,
|
|
110
|
-
wrappedExpression: wrap(text.expression),
|
|
111
|
-
})
|
|
112
|
-
}
|
|
89
|
+
// 2. Outer text effects. `texts` (elem.bindings.reactiveTexts) is produced
|
|
90
|
+
// by `collectLoopChildReactiveTexts`, which already stops descending
|
|
91
|
+
// into any reactive (slotId'd) conditional — so every entry here is
|
|
92
|
+
// genuinely outer-scope, no HTML-substring partition needed (#2347).
|
|
93
|
+
const outerTexts: ReactiveTextEffect[] = texts.map(text => ({
|
|
94
|
+
slotId: text.slotId,
|
|
95
|
+
wrappedExpression: wrap(text.expression),
|
|
96
|
+
}))
|
|
113
97
|
|
|
114
|
-
// 3. Per-conditional plans.
|
|
115
|
-
//
|
|
116
|
-
//
|
|
98
|
+
// 3. Per-conditional plans. Each arm's own reactive attrs / texts come
|
|
99
|
+
// from `LoopChildBranchSummary.reactiveAttrs` / `.reactiveTexts` —
|
|
100
|
+
// collected directly on that branch's subtree (#2347) — instead of a
|
|
101
|
+
// flat list partitioned by searching the rendered branch HTML for a
|
|
102
|
+
// `bf:<slotId>` marker. The arm bodies are fully Plan-built — no
|
|
103
|
+
// legacy passthrough.
|
|
117
104
|
const conditionalPlans: NestedConditionalPlan[] = []
|
|
118
105
|
if (conditionals) {
|
|
119
106
|
for (const cond of conditionals) {
|
|
120
|
-
const trueTexts: LoopChildArmText[] = []
|
|
121
|
-
const falseTexts: LoopChildArmText[] = []
|
|
122
|
-
for (const text of texts) {
|
|
123
|
-
if (!textSlotsInConditionals.has(text.slotId)) continue
|
|
124
|
-
const wrapped: LoopChildArmText = {
|
|
125
|
-
slotId: text.slotId,
|
|
126
|
-
wrappedExpression: wrap(text.expression),
|
|
127
|
-
}
|
|
128
|
-
if (cond.whenTrueHtml.includes(`bf:${text.slotId}`)) trueTexts.push(wrapped)
|
|
129
|
-
if (cond.whenFalseHtml.includes(`bf:${text.slotId}`)) falseTexts.push(wrapped)
|
|
130
|
-
}
|
|
131
107
|
conditionalPlans.push({
|
|
132
108
|
slotId: cond.slotId,
|
|
133
109
|
wrappedCondition: wrap(cond.condition),
|
|
134
110
|
whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
|
|
135
111
|
whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
|
|
136
|
-
whenTrueArm: buildOuterArm(cond.whenTrue,
|
|
137
|
-
whenFalseArm: buildOuterArm(cond.whenFalse,
|
|
112
|
+
whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
113
|
+
whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, profileComponentName),
|
|
138
114
|
})
|
|
139
115
|
}
|
|
140
116
|
}
|
|
@@ -149,7 +125,6 @@ export function buildReactiveEffectsPlan(
|
|
|
149
125
|
|
|
150
126
|
function buildOuterArm(
|
|
151
127
|
branch: LoopChildBranchSummary,
|
|
152
|
-
texts: readonly LoopChildArmText[],
|
|
153
128
|
wrap: (expr: string) => string,
|
|
154
129
|
loopParam: string,
|
|
155
130
|
loopParamBindings: readonly LoopParamBinding[] | undefined,
|
|
@@ -179,7 +154,8 @@ function buildOuterArm(
|
|
|
179
154
|
loopParam,
|
|
180
155
|
loopParamBindings,
|
|
181
156
|
}),
|
|
182
|
-
|
|
157
|
+
attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
|
|
158
|
+
texts: buildArmTextsPlan(branch.reactiveTexts, wrap),
|
|
183
159
|
}
|
|
184
160
|
}
|
|
185
161
|
|
|
@@ -152,8 +152,7 @@ export type BranchInnerLoopsPlan = readonly BranchInnerLoop[]
|
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* One branch-scoped reactive text effect (slotId + already-wrapped
|
|
155
|
-
* expression)
|
|
156
|
-
* `bindEvents`; recursive nested conditionals never carry texts.
|
|
155
|
+
* expression), attached inside the arm's `bindEvents`.
|
|
157
156
|
*/
|
|
158
157
|
export interface LoopChildArmText {
|
|
159
158
|
slotId: string
|
|
@@ -169,16 +168,23 @@ export interface LoopChildArmText {
|
|
|
169
168
|
* - `childComponents` — `buildBranchChildComponentInitsPlan`
|
|
170
169
|
* - `innerLoops` — `buildBranchInnerLoopsPlan`
|
|
171
170
|
* - `nestedConditionals`— `buildLoopChildConditionalPlan` (recursive)
|
|
171
|
+
* - `attrs` — reactive attrs on elements directly inside the
|
|
172
|
+
* arm, grouped by slot (#2347)
|
|
173
|
+
* - `texts` — reactive text interpolations on elements
|
|
174
|
+
* directly inside the arm (#2347)
|
|
172
175
|
*
|
|
173
|
-
* `texts`
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
+
* Both `attrs` and `texts` are collected per-arm at every nesting depth —
|
|
177
|
+
* an element inside a doubly-nested conditional gets its bindings from the
|
|
178
|
+
* innermost enclosing arm, never from an outer scope's own initial-clone
|
|
179
|
+
* query, so a branch swap (`insert()` mounting a fresh node) never leaves a
|
|
180
|
+
* binding pointed at a detached element.
|
|
176
181
|
*/
|
|
177
182
|
export interface LoopChildArmPlan {
|
|
178
183
|
events: BranchEventBindingsPlan
|
|
179
184
|
childComponents: BranchChildComponentInitsPlan
|
|
180
185
|
innerLoops: BranchInnerLoopsPlan
|
|
181
186
|
nestedConditionals: readonly LoopChildConditionalPlan[]
|
|
187
|
+
attrs: readonly import('./reactive-effects.ts').ReactiveAttrSlot[]
|
|
182
188
|
texts: readonly LoopChildArmText[]
|
|
183
189
|
}
|
|
184
190
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { varSlotId, DATA_BF_PH, keyAttrName, profileBindingId } from '../../utils.ts'
|
|
15
15
|
import { emitComponentAndEventSetup } from '../shared.ts'
|
|
16
|
+
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
16
17
|
import { templateRootIsSvg } from './template-parse.ts'
|
|
17
18
|
import { emitListenerLine } from './event-listener.ts'
|
|
18
19
|
import { nameForRegistryRef } from '../../component-scope.ts'
|
|
@@ -23,6 +24,43 @@ import type {
|
|
|
23
24
|
LoopChildArmPlan,
|
|
24
25
|
LoopChildConditionalPlan,
|
|
25
26
|
} from '../plan/loop-child-arm.ts'
|
|
27
|
+
import type { ReactiveAttrSlot } from '../plan/reactive-effects.ts'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Emit reactive attribute effects for one arm — one qsa() per slot, then one
|
|
31
|
+
* `createDisposableEffect` per attr on that slot, pushed onto the caller's
|
|
32
|
+
* `__disposers` array. Mirrors the outer renderItem-scope attr emission in
|
|
33
|
+
* `stringifyReactiveEffects` (#2347): binding attrs inside the arm that owns
|
|
34
|
+
* the element (instead of an outer scope's own initial clone) means a branch
|
|
35
|
+
* swap's fresh `insert()`-mounted node is always the one the effect targets.
|
|
36
|
+
*
|
|
37
|
+
* `createDisposableEffect` (not `createEffect`) so the caller's `bindEvents`
|
|
38
|
+
* can return a cleanup that disposes this effect on the NEXT branch swap —
|
|
39
|
+
* otherwise it would keep running against the node this branch swap just
|
|
40
|
+
* detached, and every subsequent swap stacks another orphaned effect.
|
|
41
|
+
* Callers must declare `const __disposers = []` before emitting this and
|
|
42
|
+
* return `() => __disposers.forEach(d => d())` from `bindEvents`.
|
|
43
|
+
*/
|
|
44
|
+
export function stringifyBranchReactiveAttrs(
|
|
45
|
+
lines: string[],
|
|
46
|
+
plan: readonly ReactiveAttrSlot[],
|
|
47
|
+
indent: string,
|
|
48
|
+
pc?: string,
|
|
49
|
+
): void {
|
|
50
|
+
for (const slot of plan) {
|
|
51
|
+
const varName = `__ra_${varSlotId(slot.slotId)}`
|
|
52
|
+
lines.push(`${indent}{ const ${varName} = qsa(__branchScope, '[bf="${slot.slotId}"]')`)
|
|
53
|
+
lines.push(`${indent}if (${varName}) {`)
|
|
54
|
+
for (const attr of slot.attrs) {
|
|
55
|
+
lines.push(`${indent} __disposers.push(createDisposableEffect(() => {`)
|
|
56
|
+
for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
|
|
57
|
+
lines.push(`${indent} ${stmt}`)
|
|
58
|
+
}
|
|
59
|
+
lines.push(`${indent} }${profileBindingId(pc, slot.slotId)}))`)
|
|
60
|
+
}
|
|
61
|
+
lines.push(`${indent}} }`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
26
64
|
|
|
27
65
|
/**
|
|
28
66
|
* Emit `addEventListener` setup for a loop-cond branch arm. One qsa() per
|
|
@@ -176,7 +214,15 @@ function stringifyLoopChildConditional(
|
|
|
176
214
|
lines.push(`${indent}}${profileBindingId(pc, cond.slotId)})`)
|
|
177
215
|
}
|
|
178
216
|
|
|
179
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Emit one arm's full body: events, child component inits, inner loops,
|
|
219
|
+
* then a disposable section (reactive attrs, nested conditionals, texts)
|
|
220
|
+
* whose aggregate cleanup `bindEvents` returns to `insert()` (#2347 follow-up).
|
|
221
|
+
* Shared by both the loop-scoped-conditional stringifier in this file and
|
|
222
|
+
* the outer (top-of-loop-item) conditional stringifier in
|
|
223
|
+
* `reactive-effects.ts` — the two arm shapes are identical.
|
|
224
|
+
*/
|
|
225
|
+
export function stringifyLoopChildArm(
|
|
180
226
|
lines: string[],
|
|
181
227
|
arm: LoopChildArmPlan,
|
|
182
228
|
armIndent: string,
|
|
@@ -185,10 +231,38 @@ function stringifyLoopChildArm(
|
|
|
185
231
|
stringifyBranchEventBindings(lines, arm.events, armIndent)
|
|
186
232
|
stringifyBranchChildComponentInits(lines, arm.childComponents, armIndent)
|
|
187
233
|
stringifyBranchInnerLoops(lines, arm.innerLoops, armIndent, pc)
|
|
188
|
-
|
|
234
|
+
|
|
235
|
+
// Disposable section: reactive attrs + text effects + nested conditionals.
|
|
236
|
+
// `bindEvents` returns the aggregate cleanup so `insert()` disposes this
|
|
237
|
+
// arm's scoped effects (and any nested insert()'s own effect tree) before
|
|
238
|
+
// the NEXT branch swap — otherwise they'd keep running against the node
|
|
239
|
+
// this swap just detached, stacking another orphaned effect on every
|
|
240
|
+
// subsequent toggle (#2347 follow-up).
|
|
241
|
+
const hasDisposables = arm.attrs.length > 0 || arm.texts.length > 0 || arm.nestedConditionals.length > 0
|
|
242
|
+
if (!hasDisposables) return
|
|
243
|
+
|
|
244
|
+
lines.push(`${armIndent}const __disposers = []`)
|
|
245
|
+
stringifyBranchReactiveAttrs(lines, arm.attrs, armIndent, pc)
|
|
246
|
+
|
|
247
|
+
// Nested conditionals: each inner `insert()` is wrapped in its own
|
|
248
|
+
// disposable effect so disposing THIS entry cascades to whatever
|
|
249
|
+
// reactive state that insert() set up internally (mirrors the top-level
|
|
250
|
+
// conditional's nested-conditional handling in stringify/insert.ts).
|
|
251
|
+
for (const cond of arm.nestedConditionals) {
|
|
252
|
+
lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => {`)
|
|
253
|
+
stringifyLoopChildConditional(lines, cond, `${armIndent} `, pc)
|
|
254
|
+
lines.push(`${armIndent}}))`)
|
|
255
|
+
}
|
|
256
|
+
|
|
189
257
|
for (const text of arm.texts) {
|
|
258
|
+
// __bfText (not a naive `.textContent = String(...)`) so a Child-position
|
|
259
|
+
// expression whose value is a live Node (e.g. a hoisted `renderNode={(n)
|
|
260
|
+
// => <PillNode/>}` callback, #1213) is spliced into the slot by identity
|
|
261
|
+
// instead of being stringified to "[object HTMLElement]" (#2347).
|
|
190
262
|
const varName = `__rt_${varSlotId(text.slotId)}`
|
|
191
|
-
lines.push(`${armIndent}
|
|
192
|
-
lines.push(`${armIndent}
|
|
263
|
+
lines.push(`${armIndent}let ${varName} = $t(__branchScope, '${text.slotId}')[0]`)
|
|
264
|
+
lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => { ${varName} = __bfText(${varName}, ${text.wrappedExpression}) }${profileBindingId(pc, text.slotId)}))`)
|
|
193
265
|
}
|
|
266
|
+
|
|
267
|
+
lines.push(`${armIndent}return () => __disposers.forEach(d => d())`)
|
|
194
268
|
}
|
|
@@ -10,13 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { varSlotId, profileBindingId } from '../../utils.ts'
|
|
12
12
|
import { emitAttrUpdate } from '../../emit-reactive.ts'
|
|
13
|
-
import {
|
|
14
|
-
stringifyBranchChildComponentInits,
|
|
15
|
-
stringifyBranchEventBindings,
|
|
16
|
-
stringifyBranchInnerLoops,
|
|
17
|
-
stringifyLoopChildConditionals,
|
|
18
|
-
} from './loop-child-arm.ts'
|
|
19
|
-
import type { LoopChildArmPlan, LoopChildArmText } from '../plan/loop-child-arm.ts'
|
|
13
|
+
import { stringifyLoopChildArm } from './loop-child-arm.ts'
|
|
20
14
|
import type {
|
|
21
15
|
NestedConditionalPlan,
|
|
22
16
|
ReactiveEffectsPlan,
|
|
@@ -126,28 +120,12 @@ function emitOuterConditional(
|
|
|
126
120
|
lines.push(`${indent}insert(${elVar}, '${cond.slotId}', () => ${cond.wrappedCondition}, {`)
|
|
127
121
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenTrueTemplateHtml}\`, slots: __slots } },`)
|
|
128
122
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
129
|
-
|
|
123
|
+
stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc)
|
|
130
124
|
lines.push(`${indent} }`)
|
|
131
125
|
lines.push(`${indent}}, {`)
|
|
132
126
|
lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenFalseTemplateHtml}\`, slots: __slots } },`)
|
|
133
127
|
lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`)
|
|
134
|
-
|
|
128
|
+
stringifyLoopChildArm(lines, cond.whenFalseArm, armIndent, pc)
|
|
135
129
|
lines.push(`${indent} }`)
|
|
136
130
|
lines.push(`${indent}}${profileBindingId(pc, cond.slotId)})`)
|
|
137
131
|
}
|
|
138
|
-
|
|
139
|
-
function emitArmBody(lines: string[], arm: LoopChildArmPlan, armIndent: string, pc: string | undefined): void {
|
|
140
|
-
stringifyBranchEventBindings(lines, arm.events, armIndent)
|
|
141
|
-
stringifyBranchChildComponentInits(lines, arm.childComponents, armIndent)
|
|
142
|
-
stringifyBranchInnerLoops(lines, arm.innerLoops, armIndent, pc)
|
|
143
|
-
stringifyLoopChildConditionals(lines, arm.nestedConditionals, armIndent, pc)
|
|
144
|
-
for (const text of arm.texts) {
|
|
145
|
-
emitArmText(lines, armIndent, text, pc)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function emitArmText(lines: string[], indent: string, text: LoopChildArmText, pc: string | undefined): void {
|
|
150
|
-
const varName = `__rt_${varSlotId(text.slotId)}`
|
|
151
|
-
lines.push(`${indent}{ const [${varName}] = $t(__branchScope, '${text.slotId}')`)
|
|
152
|
-
lines.push(`${indent}if (${varName}) createEffect(() => { ${varName}.textContent = String(${text.wrappedExpression}) }${profileBindingId(pc, text.slotId)}) }`)
|
|
153
|
-
}
|
|
@@ -265,14 +265,32 @@ export function collectEventHandlersFromIR(node: IRNode): string[] {
|
|
|
265
265
|
/**
|
|
266
266
|
* Traverse an IR tree depth-first, calling visitor for each element node.
|
|
267
267
|
* Shared by collectConditionalBranchEvents, collectConditionalBranchRefs,
|
|
268
|
-
* and collectLoopChildEvents to avoid duplicating the
|
|
268
|
+
* collectLoopChildRefs, and collectLoopChildEvents to avoid duplicating the
|
|
269
|
+
* traversal logic.
|
|
269
270
|
*
|
|
270
271
|
* 'loop' is intentionally skipped. Nested .map() event delegation requires
|
|
271
272
|
* a different approach (nested data-key lookup + inner loop variable
|
|
272
273
|
* resolution) that isn't implemented yet. See memory:
|
|
273
274
|
* compiler-reconcile-templates-events.md
|
|
275
|
+
*
|
|
276
|
+
* `stopAtReactiveConditionals` (#2347): when true, a nested `conditional`
|
|
277
|
+
* with a `slotId` is NOT descended into — the caller is one that also
|
|
278
|
+
* separately collects that nested conditional via `collectBranchConditionals`
|
|
279
|
+
* / `collectLoopChildConditionals` and gives it its own `insert()` call, so
|
|
280
|
+
* descending here too would double-bind (against the enclosing scope's own
|
|
281
|
+
* initial template node, in addition to the nested conditional's own
|
|
282
|
+
* `bindEvents` against whatever node `insert()` mounts). Defaults to false
|
|
283
|
+
* because several callers (e.g. a plain nested `.map()`'s own per-item
|
|
284
|
+
* bindings, `collectInnerLoops`'s non-branch path) have no such sibling
|
|
285
|
+
* collector — for those, a slotId'd conditional's content is ONLY ever
|
|
286
|
+
* reachable by unconditionally descending here, since it's inlined directly
|
|
287
|
+
* into that scope's template rather than switched via its own `insert()`.
|
|
274
288
|
*/
|
|
275
|
-
function traverseElements(
|
|
289
|
+
function traverseElements(
|
|
290
|
+
node: IRNode,
|
|
291
|
+
visitor: (el: IRElement, domDepth: number) => void,
|
|
292
|
+
stopAtReactiveConditionals = false,
|
|
293
|
+
): void {
|
|
276
294
|
walkIR(node, 0, {
|
|
277
295
|
element: ({ node: el, scope: domDepth, descend }) => {
|
|
278
296
|
visitor(el, domDepth)
|
|
@@ -281,12 +299,25 @@ function traverseElements(node: IRNode, visitor: (el: IRElement, domDepth: numbe
|
|
|
281
299
|
loop: () => {
|
|
282
300
|
// skip: nested-loop event delegation is handled separately
|
|
283
301
|
},
|
|
302
|
+
...(stopAtReactiveConditionals
|
|
303
|
+
? {
|
|
304
|
+
conditional: ({ node: c, scope: domDepth, descend }: { node: IRNode & { type: 'conditional' }; scope: number; descend: (s: number) => void }) => {
|
|
305
|
+
if (!c.slotId) descend(domDepth)
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
284
309
|
})
|
|
285
310
|
}
|
|
286
311
|
|
|
287
312
|
/**
|
|
288
313
|
* Collect events from a conditional branch for use with insert().
|
|
289
314
|
* These events will be bound via the branch's bindEvents function.
|
|
315
|
+
*
|
|
316
|
+
* Always stops at a nested reactive conditional (#2347): every caller
|
|
317
|
+
* (`summarizeBranch`, `summarizeLoopChildBranch`) also collects that nested
|
|
318
|
+
* conditional separately (`collectBranchConditionals` /
|
|
319
|
+
* `collectLoopChildConditionals`), so its own `insert()`/bindEvents owns
|
|
320
|
+
* its events.
|
|
290
321
|
*/
|
|
291
322
|
export function collectConditionalBranchEvents(node: IRNode): ConditionalBranchEvent[] {
|
|
292
323
|
const events: ConditionalBranchEvent[] = []
|
|
@@ -300,13 +331,17 @@ export function collectConditionalBranchEvents(node: IRNode): ConditionalBranchE
|
|
|
300
331
|
})
|
|
301
332
|
}
|
|
302
333
|
}
|
|
303
|
-
})
|
|
334
|
+
}, true)
|
|
304
335
|
return events
|
|
305
336
|
}
|
|
306
337
|
|
|
307
338
|
/**
|
|
308
339
|
* Collect refs from a conditional branch for use with insert().
|
|
309
340
|
* These refs will be called via the branch's bindEvents function.
|
|
341
|
+
*
|
|
342
|
+
* Always stops at a nested reactive conditional (#2347): its sole caller
|
|
343
|
+
* (`summarizeBranch`) also collects that nested conditional separately via
|
|
344
|
+
* `collectBranchConditionals`, so its own `insert()`/bindEvents owns its refs.
|
|
310
345
|
*/
|
|
311
346
|
export function collectConditionalBranchRefs(node: IRNode): ConditionalBranchRef[] {
|
|
312
347
|
const refs: ConditionalBranchRef[] = []
|
|
@@ -317,7 +352,7 @@ export function collectConditionalBranchRefs(node: IRNode): ConditionalBranchRef
|
|
|
317
352
|
callback: el.ref,
|
|
318
353
|
})
|
|
319
354
|
}
|
|
320
|
-
})
|
|
355
|
+
}, true)
|
|
321
356
|
return refs
|
|
322
357
|
}
|
|
323
358
|
|
|
@@ -511,12 +546,21 @@ function traverseForComponents(
|
|
|
511
546
|
* Includes expressions that read signals OR reference the loop parameter.
|
|
512
547
|
* With per-item signals, loop param access (item().text) is reactive
|
|
513
548
|
* because item is a signal accessor.
|
|
549
|
+
*
|
|
550
|
+
* `stopAtReactiveConditionals` (#2347): pass true only when the caller also
|
|
551
|
+
* separately collects nested reactive conditionals (via
|
|
552
|
+
* `collectLoopChildConditionals`) and gives each its own `insert()` /
|
|
553
|
+
* bindEvents — otherwise a text inside such a conditional would bind twice.
|
|
554
|
+
* Defaults to false: some callers (e.g. a plain nested `.map()`'s own
|
|
555
|
+
* per-item bindings) have no sibling conditional collector, so a slotId'd
|
|
556
|
+
* conditional's inlined content is only ever reachable by descending here.
|
|
514
557
|
*/
|
|
515
558
|
export function collectLoopChildReactiveTexts(
|
|
516
559
|
node: IRNode,
|
|
517
560
|
ctx: ClientJsContext,
|
|
518
561
|
loopParam?: string,
|
|
519
562
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
563
|
+
stopAtReactiveConditionals = false,
|
|
520
564
|
): LoopChildReactiveText[] {
|
|
521
565
|
const texts: LoopChildReactiveText[] = []
|
|
522
566
|
walkIR(node, false, {
|
|
@@ -549,8 +593,13 @@ export function collectLoopChildReactiveTexts(
|
|
|
549
593
|
...(expanded.freeIds !== undefined && { freeIdentifiers: expanded.freeIds }),
|
|
550
594
|
})
|
|
551
595
|
},
|
|
552
|
-
conditional: ({ descend }) => {
|
|
553
|
-
|
|
596
|
+
conditional: ({ node: c, descend }) => {
|
|
597
|
+
// When stopAtReactiveConditionals is set, a reactive conditional
|
|
598
|
+
// (slotId set) gets its own insert() call that rebinds its branch
|
|
599
|
+
// text effects independently — descending here too would bind the
|
|
600
|
+
// same expression twice, once against the enclosing scope's initial
|
|
601
|
+
// node and once against insert()'s mounted node (#2347).
|
|
602
|
+
if (!stopAtReactiveConditionals || !c.slotId) descend(true)
|
|
554
603
|
},
|
|
555
604
|
})
|
|
556
605
|
return texts
|
|
@@ -560,12 +609,17 @@ export function collectLoopChildReactiveTexts(
|
|
|
560
609
|
* Collect reactive attributes from loop children.
|
|
561
610
|
* These are dynamic attributes that read signals and need createEffect
|
|
562
611
|
* to update the DOM when signals change.
|
|
612
|
+
*
|
|
613
|
+
* `stopAtReactiveConditionals` (#2347): see `collectLoopChildReactiveTexts` —
|
|
614
|
+
* same semantics. Pass true only when the caller also separately collects
|
|
615
|
+
* nested reactive conditionals and gives each its own `insert()`.
|
|
563
616
|
*/
|
|
564
617
|
export function collectLoopChildReactiveAttrs(
|
|
565
618
|
node: IRNode,
|
|
566
619
|
ctx: ClientJsContext,
|
|
567
620
|
loopParam?: string,
|
|
568
621
|
loopParamBindings?: readonly LoopParamBinding[],
|
|
622
|
+
stopAtReactiveConditionals = false,
|
|
569
623
|
): LoopChildReactiveAttr[] {
|
|
570
624
|
const attrs: LoopChildReactiveAttr[] = []
|
|
571
625
|
traverseElements(node, (el) => {
|
|
@@ -617,6 +671,6 @@ export function collectLoopChildReactiveAttrs(
|
|
|
617
671
|
})
|
|
618
672
|
}
|
|
619
673
|
}
|
|
620
|
-
})
|
|
674
|
+
}, stopAtReactiveConditionals)
|
|
621
675
|
return attrs
|
|
622
676
|
}
|
|
@@ -512,6 +512,19 @@ export interface LoopChildBranchSummary {
|
|
|
512
512
|
conditionals?: LoopChildConditional[]
|
|
513
513
|
/** Events on elements inside the branch — attached via insert() bindEvents (#839). */
|
|
514
514
|
events?: ConditionalBranchEvent[]
|
|
515
|
+
/**
|
|
516
|
+
* Reactive attrs on elements directly inside the branch — attached via
|
|
517
|
+
* insert() bindEvents (#2347). Collected loop-param-aware so `u().active`
|
|
518
|
+
* style loop-item accessors classify as reactive; stops at any further
|
|
519
|
+
* nested reactive conditional (which gets its own insert() + arm).
|
|
520
|
+
*/
|
|
521
|
+
reactiveAttrs?: LoopChildReactiveAttr[]
|
|
522
|
+
/**
|
|
523
|
+
* Reactive text interpolations on elements directly inside the branch —
|
|
524
|
+
* attached via insert() bindEvents (#2347). Same loop-param-aware /
|
|
525
|
+
* nested-conditional-stopping semantics as `reactiveAttrs`.
|
|
526
|
+
*/
|
|
527
|
+
reactiveTexts?: LoopChildReactiveText[]
|
|
515
528
|
}
|
|
516
529
|
|
|
517
530
|
export interface LoopChildConditional {
|