@barefootjs/jsx 0.25.0 → 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/index.js +248 -110
- 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 +8 -4
- 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 +252 -1
- package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
- package/src/adapters/dangerous-inner-html.ts +101 -48
- package/src/analyzer.ts +222 -58
- 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 +8 -4
|
@@ -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 {
|
package/src/rich-type-refusal.ts
CHANGED
|
@@ -99,10 +99,11 @@ function pushDiagnostic(
|
|
|
99
99
|
// `toLocaleDateString` has a catalogued explicit-input form (#2324 slice
|
|
100
100
|
// 2) — point the fix at it instead of the generic escape hatches alone.
|
|
101
101
|
// The implicit-environment forms (zero-arg, locale-only, non-literal
|
|
102
|
-
// locale,
|
|
102
|
+
// locale, an unverifiable timeZone literal) stay refused by design;
|
|
103
|
+
// canonical IANA zone literals compile since #2344.
|
|
103
104
|
const suggestion =
|
|
104
105
|
method === 'toLocaleDateString' && typeName === 'Date'
|
|
105
|
-
? "Pass a literal locale and an explicit literal timeZone — .toLocaleDateString('ja-JP', { timeZone: 'UTC' })
|
|
106
|
+
? "Pass a literal locale and an explicit literal timeZone — .toLocaleDateString('ja-JP', { timeZone: 'UTC' }), a fixed '±HH:MM' offset, or a canonical IANA zone ID like 'Asia/Tokyo' (exact case; #2344) — to compile it to the format_date helper; for a runtime locale, resolve the pattern in your i18n layer and use formatDate(date, pattern, tz) from @barefootjs/client. Alternatively add /* @client */ or pre-compute server-side."
|
|
106
107
|
: 'Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side.'
|
|
107
108
|
errors.push({
|
|
108
109
|
code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
|
|
@@ -34,8 +34,15 @@
|
|
|
34
34
|
* there, feeding `formatDate` directly. An OPTIONAL union prop also
|
|
35
35
|
* declines: `undefined` at runtime makes real `toLocaleDateString` read
|
|
36
36
|
* the host locale, which no frozen pattern table can reproduce;
|
|
37
|
-
* - an IANA `timeZone`
|
|
38
|
-
*
|
|
37
|
+
* - an IANA `timeZone` literal the build machine cannot verify (#2344):
|
|
38
|
+
* a named zone IS admitted when the build's own `Intl` echoes it back
|
|
39
|
+
* verbatim from `resolvedOptions().timeZone` — the canonical primary
|
|
40
|
+
* ID, the one spelling every backend's tzdata resolves identically (the
|
|
41
|
+
* runtime helpers resolve it through Go `time.LoadLocation`, Ruby
|
|
42
|
+
* tzinfo, PHP `DateTimeZone`, Perl `DateTime::TimeZone`, Python
|
|
43
|
+
* `zoneinfo`, Rust `chrono-tz`). Non-canonical case, link/alias names
|
|
44
|
+
* the probe canonicalizes away, and unknown zones all decline — the
|
|
45
|
+
* probe-and-verify discipline of #2334, applied to tz;
|
|
39
46
|
* - an options bag the probe cannot reproduce EXACTLY in the token+table
|
|
40
47
|
* vocabulary — era, dayPeriod, 2-digit year, narrow name forms,
|
|
41
48
|
* non-literal option values ("faithful or loud", never approximate);
|
|
@@ -57,17 +64,46 @@ import { resolveReceiverType, baseTypeName } from './rich-type-evidence.ts'
|
|
|
57
64
|
import { typeReachesDate } from './date-lowering.ts'
|
|
58
65
|
|
|
59
66
|
/**
|
|
60
|
-
* `timeZone` literals the lowering admits: `'UTC'` or a
|
|
61
|
-
* offset **within ECMA-402's valid offset range** (hours
|
|
62
|
-
* 00–59). An out-of-range shape like `'+25:00'` or
|
|
63
|
-
* (→ BF021), not lower: real `toLocaleDateString`
|
|
64
|
-
* it, so compiling it would render a nonsense
|
|
65
|
-
* adapters while the JS-native path (Hono, and the
|
|
66
|
-
* the sugar stands in for) crashes — the exact
|
|
67
|
-
* to rule out.
|
|
67
|
+
* `timeZone` literals the lowering admits without probing: `'UTC'` or a
|
|
68
|
+
* fixed `±HH:MM` offset **within ECMA-402's valid offset range** (hours
|
|
69
|
+
* 00–23, minutes 00–59). An out-of-range shape like `'+25:00'` or
|
|
70
|
+
* `'+99:99'` must DECLINE (→ BF021), not lower: real `toLocaleDateString`
|
|
71
|
+
* throws a RangeError on it, so compiling it would render a nonsense
|
|
72
|
+
* offset on the template adapters while the JS-native path (Hono, and the
|
|
73
|
+
* pre-rewrite semantics the sugar stands in for) crashes — the exact
|
|
74
|
+
* divergence the sugar exists to rule out. Named IANA zones are admitted
|
|
75
|
+
* through {@link isBuildResolvableTimeZone} instead (#2344).
|
|
68
76
|
*/
|
|
69
77
|
export const TO_LOCALE_TZ_RE = /^(?:UTC|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/
|
|
70
78
|
|
|
79
|
+
/** Build-time probe cache: tz literal → verified-canonical? */
|
|
80
|
+
const tzProbeCache = new Map<string, boolean>()
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Probe-and-verify a named-zone `timeZone` literal against the build
|
|
84
|
+
* machine's own `Intl` (#2344, the #2334 discipline applied to tz): admit
|
|
85
|
+
* it iff `resolvedOptions().timeZone` echoes the literal back VERBATIM.
|
|
86
|
+
* The echo check is what pins the canonical primary IANA ID — `Intl`
|
|
87
|
+
* itself accepts case-folded spellings and link/alias names but
|
|
88
|
+
* canonicalizes them in `resolvedOptions()`, and those non-canonical
|
|
89
|
+
* spellings are exactly where backend tzdata layers disagree (Go/Python/
|
|
90
|
+
* Ruby/Rust resolve the exact ID set; PHP folds case; link availability
|
|
91
|
+
* varies by tzdata vintage). Anything the probe can't verify declines
|
|
92
|
+
* (→ BF021) — the `formatDate` primitive stays available for edge cases.
|
|
93
|
+
*/
|
|
94
|
+
export function isBuildResolvableTimeZone(value: string): boolean {
|
|
95
|
+
const cached = tzProbeCache.get(value)
|
|
96
|
+
if (cached !== undefined) return cached
|
|
97
|
+
let verified: boolean
|
|
98
|
+
try {
|
|
99
|
+
verified = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone === value
|
|
100
|
+
} catch {
|
|
101
|
+
verified = false
|
|
102
|
+
}
|
|
103
|
+
tzProbeCache.set(value, verified)
|
|
104
|
+
return verified
|
|
105
|
+
}
|
|
106
|
+
|
|
71
107
|
/**
|
|
72
108
|
* Probe instant for pattern derivation: 2001-02-03 UTC. Month and day are
|
|
73
109
|
* distinct single-digit values, so the rendered part text distinguishes both
|
|
@@ -452,8 +488,9 @@ export function matchToLocaleDateStringCall(
|
|
|
452
488
|
if (callee.kind !== 'member' || callee.computed) return null
|
|
453
489
|
if (callee.property !== 'toLocaleDateString' || args.length !== 2) return null
|
|
454
490
|
const [locale, options] = args
|
|
455
|
-
// Options bag: `timeZone` is REQUIRED (a literal 'UTC' | valid ±HH:MM
|
|
456
|
-
//
|
|
491
|
+
// Options bag: `timeZone` is REQUIRED (a literal 'UTC' | valid ±HH:MM |
|
|
492
|
+
// probe-verified canonical IANA zone (#2344) — its omission would read
|
|
493
|
+
// the host timezone); every OTHER key rides into
|
|
457
494
|
// the Intl probe as-is when its value is a string literal (#2334's
|
|
458
495
|
// fidelity rule: admit any literal options bag the probe can reproduce
|
|
459
496
|
// exactly, decline everything else — dateStyle, month/weekday forms, …).
|
|
@@ -464,7 +501,7 @@ export function matchToLocaleDateStringCall(
|
|
|
464
501
|
if (prop.value.kind !== 'literal' || prop.value.literalType !== 'string') return null
|
|
465
502
|
const value = String(prop.value.value)
|
|
466
503
|
if (prop.key === 'timeZone') {
|
|
467
|
-
if (!TO_LOCALE_TZ_RE.test(value)) return null
|
|
504
|
+
if (!TO_LOCALE_TZ_RE.test(value) && !isBuildResolvableTimeZone(value)) return null
|
|
468
505
|
tz = value
|
|
469
506
|
} else {
|
|
470
507
|
probeOptions[prop.key] = value
|
package/src/types.ts
CHANGED
|
@@ -1525,10 +1525,11 @@ export interface ReactiveFactoryInfo {
|
|
|
1525
1525
|
|
|
1526
1526
|
/**
|
|
1527
1527
|
* An import the helper file holds that an inlined factory body references
|
|
1528
|
-
* (#2332). Not a BF112 capture: the component
|
|
1529
|
-
* binding itself. Collected at prescan with the
|
|
1530
|
-
* relative to the component file (or unchanged
|
|
1531
|
-
* the source rewriter injects one deduped import
|
|
1528
|
+
* (#2332, type-only refs added #2350). Not a BF112 capture: the component
|
|
1529
|
+
* file can import the same binding itself. Collected at prescan with the
|
|
1530
|
+
* specifier ALREADY rewritten relative to the component file (or unchanged
|
|
1531
|
+
* for bare/npm specifiers); the source rewriter injects one deduped import
|
|
1532
|
+
* statement per (specifier, isTypeOnly) pair.
|
|
1532
1533
|
*/
|
|
1533
1534
|
export interface RequiredFactoryImport {
|
|
1534
1535
|
/** Local binding name as referenced inside the factory body. */
|
|
@@ -1537,6 +1538,9 @@ export interface RequiredFactoryImport {
|
|
|
1537
1538
|
exportedName: string
|
|
1538
1539
|
/** Component-file-relative specifier (`../lib/mathmod`), or the unchanged bare specifier. */
|
|
1539
1540
|
specifier: string
|
|
1541
|
+
/** True when the factory body only ever references this in type position —
|
|
1542
|
+
* re-provisioned as `import type { ... }` instead of a value import. */
|
|
1543
|
+
isTypeOnly?: boolean
|
|
1540
1544
|
}
|
|
1541
1545
|
|
|
1542
1546
|
/**
|