@barefootjs/jsx 0.34.0 → 0.35.1
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/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1072 -903
- package/dist/ir-to-client-js/collect-elements.d.ts +22 -2
- package/dist/ir-to-client-js/collect-elements.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-inner-loop.d.ts +14 -2
- 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-lazy-row.d.ts +11 -0
- package/dist/ir-to-client-js/control-flow/plan/build-lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +6 -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-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts +52 -0
- package/dist/ir-to-client-js/control-flow/plan/build-plain-row.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +5 -2
- 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/inner-loop.d.ts +24 -44
- package/dist/ir-to-client-js/control-flow/plan/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts +6 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-conditional.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts +5 -7
- package/dist/ir-to-client-js/control-flow/plan/lazy-preamble.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/lazy-row-eligibility.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +14 -0
- 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/plan/loop.d.ts +10 -6
- 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 +11 -9
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/composite-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts +6 -14
- package/dist/ir-to-client-js/control-flow/stringify/inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts +1 -3
- package/dist/ir-to-client-js/control-flow/stringify/lazy-row.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +19 -3
- 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/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts +5 -1
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +16 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-reactive.d.ts +54 -0
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/emit-registration.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +2 -2
- 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/index.d.ts.map +1 -1
- package/dist/ir-to-client-js/markup-slots.d.ts +21 -0
- package/dist/ir-to-client-js/markup-slots.d.ts.map +1 -0
- package/dist/ir-to-client-js/reactivity.d.ts +62 -18
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/safe-html.d.ts +158 -0
- package/dist/ir-to-client-js/safe-html.d.ts.map +1 -0
- package/dist/ir-to-client-js/types.d.ts +18 -2
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +34 -1
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/prop-rewrite.d.ts +8 -3
- package/dist/prop-rewrite.d.ts.map +1 -1
- package/dist/props-binding.d.ts +22 -1
- package/dist/props-binding.d.ts.map +1 -1
- package/dist/reactivity-checker.d.ts +18 -0
- package/dist/reactivity-checker.d.ts.map +1 -1
- package/dist/ssr-defaults.d.ts.map +1 -1
- package/dist/types.d.ts +5 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +138 -22
- package/src/__tests__/binding-scope-ratchet.test.ts +25 -2
- package/src/__tests__/build-plain-row-consolidation.test.ts +120 -0
- package/src/__tests__/child-prop-fallback-wrap.test.ts +4 -2
- package/src/__tests__/client-template-escape-soundness.test.ts +538 -0
- package/src/__tests__/compiler-stress-1244.test.ts +8 -8
- package/src/__tests__/csr-template-loop-shadowing.test.ts +4 -2
- package/src/__tests__/flatmap-segments.test.ts +1 -1
- package/src/__tests__/issue-2868-cond-arm-tag-collision.test.ts +185 -0
- package/src/__tests__/issue-2869-attr-dedup-guard.test.ts +141 -0
- package/src/__tests__/lazy-conditional.test.ts +52 -12
- package/src/__tests__/lazy-preamble.test.ts +11 -11
- package/src/__tests__/lazy-row-eligibility.test.ts +8 -3
- package/src/__tests__/loop-child-reactive-attr-const-shadow.test.ts +25 -3
- package/src/__tests__/loop-index-reactivity.test.ts +208 -0
- package/src/__tests__/loop-item-conditional-codegen.test.ts +4 -2
- package/src/__tests__/map-arbitrary-body.test.ts +1 -1
- package/src/__tests__/map-body-brand-checker.test.ts +176 -0
- package/src/__tests__/nested-loop-index-param.test.ts +41 -8
- package/src/__tests__/nested-loop-non-outer-reactivity.test.ts +177 -0
- package/src/__tests__/nested-loop-plain.test.ts +39 -25
- package/src/__tests__/nested-loop-reactive-attrs.test.ts +16 -6
- package/src/__tests__/preamble-branch-arm-rerun.test.ts +135 -0
- package/src/__tests__/reactive-attrs-in-map.test.ts +13 -10
- package/src/__tests__/rewrite-destructured-props.test.ts +37 -0
- package/src/__tests__/safe-html-splice-door.test.ts +61 -0
- package/src/__tests__/ssr-defaults.test.ts +59 -0
- package/src/__tests__/style-css-var-reactive.test.ts +4 -1
- package/src/analyzer-context.ts +12 -10
- package/src/analyzer.ts +4 -0
- package/src/index.ts +8 -2
- package/src/ir-to-client-js/collect-elements.ts +126 -32
- package/src/ir-to-client-js/control-flow/plan/build-branch-loop.ts +20 -28
- package/src/ir-to-client-js/control-flow/plan/build-component-loop.ts +9 -7
- package/src/ir-to-client-js/control-flow/plan/build-composite-loop.ts +12 -6
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +36 -59
- package/src/ir-to-client-js/control-flow/plan/build-lazy-row.ts +25 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +36 -9
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +35 -35
- package/src/ir-to-client-js/control-flow/plan/build-plain-row.ts +116 -0
- package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +23 -8
- package/src/ir-to-client-js/control-flow/plan/inner-loop.ts +24 -45
- package/src/ir-to-client-js/control-flow/plan/lazy-conditional.ts +11 -10
- package/src/ir-to-client-js/control-flow/plan/lazy-preamble.ts +11 -13
- package/src/ir-to-client-js/control-flow/plan/lazy-row-eligibility.ts +13 -3
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +14 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +10 -6
- package/src/ir-to-client-js/control-flow/shared.ts +40 -17
- package/src/ir-to-client-js/control-flow/stringify/composite-loop.ts +2 -1
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +20 -66
- package/src/ir-to-client-js/control-flow/stringify/insert.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/lazy-row.ts +22 -15
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +53 -11
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +4 -2
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +29 -18
- package/src/ir-to-client-js/csr-substitute.ts +29 -0
- package/src/ir-to-client-js/emit-reactive.ts +92 -6
- package/src/ir-to-client-js/emit-registration.ts +2 -4
- package/src/ir-to-client-js/html-template.ts +67 -245
- package/src/ir-to-client-js/imports.ts +3 -0
- package/src/ir-to-client-js/index.ts +4 -4
- package/src/ir-to-client-js/markup-slots.ts +25 -0
- package/src/ir-to-client-js/reactivity.ts +73 -27
- package/src/ir-to-client-js/safe-html.ts +243 -0
- package/src/ir-to-client-js/types.ts +19 -3
- package/src/ir-to-client-js/utils.ts +58 -10
- package/src/jsx-to-ir.ts +136 -24
- package/src/prop-rewrite.ts +8 -4
- package/src/props-binding.ts +37 -1
- package/src/reactivity-checker.ts +60 -0
- package/src/ssr-defaults.ts +45 -0
- package/src/types.ts +5 -10
- package/src/__tests__/text-slot-escaping.test.ts +0 -65
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2861: a `.map()` row's index-derived output stays live even when the
|
|
3
|
+
* expression reads NOTHING else reactive — no item, no signal, no memo, no
|
|
4
|
+
* function call. `String(i + 1)`-shaped expressions were already fixed by
|
|
5
|
+
* #2859/#2860 (they trip the AST-flag function-call fallback); this file
|
|
6
|
+
* covers the residual gap `classifyReactivity` had no case for at all: a
|
|
7
|
+
* bare `{i}` text, an `i % 2 === 0 ? … : …` class/conditional, and the
|
|
8
|
+
* same shapes one level deeper when the index in question belongs to an
|
|
9
|
+
* OUTER loop, not the row's own.
|
|
10
|
+
*
|
|
11
|
+
* Root cause (see `reactivity.ts`'s `ReactivitySource`/`classifyReactivity`
|
|
12
|
+
* doc comments): Phase 1 (`jsx-to-ir.ts`'s `referencesLoopParam`, via
|
|
13
|
+
* `BindingScope.valueBoundNames()`) already grants such an expression a
|
|
14
|
+
* patchable slot — item/index/destructure are graneted a slot together —
|
|
15
|
+
* but Phase 2's `classifyReactivity` had no `loop-index` case mirroring its
|
|
16
|
+
* existing `loop-param` one, so the slot was written once at row-creation
|
|
17
|
+
* time and never revisited.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, test, expect } from 'bun:test'
|
|
21
|
+
import { compileJSX } from '../compiler'
|
|
22
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
23
|
+
|
|
24
|
+
const adapter = new TestAdapter()
|
|
25
|
+
|
|
26
|
+
function clientJsFor(source: string): string {
|
|
27
|
+
const result = compileJSX(source, 'Repro.tsx', { adapter })
|
|
28
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
29
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
30
|
+
expect(clientJs).toBeDefined()
|
|
31
|
+
return clientJs!.content
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('plain loop: pure index-only expressions (#2861)', () => {
|
|
35
|
+
test('bare {i} text wires reactively through the lazy row graph', () => {
|
|
36
|
+
const content = clientJsFor(`
|
|
37
|
+
'use client'
|
|
38
|
+
import { createSignal } from '@barefootjs/client'
|
|
39
|
+
export function Repro() {
|
|
40
|
+
const [items] = createSignal([{ id: 'a' }, { id: 'b' }, { id: 'c' }])
|
|
41
|
+
return (
|
|
42
|
+
<ul>
|
|
43
|
+
{items().map((item, i) => (
|
|
44
|
+
<li key={item.id}>{i}</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
`)
|
|
50
|
+
|
|
51
|
+
// No refs/child components/inner loops — this row is lazy-eligible, and
|
|
52
|
+
// the widened `classifyLazyBinding` (#2859/#2860 stacked work) already
|
|
53
|
+
// routes an index-only binding into applyItem/indexDriven once it's
|
|
54
|
+
// collected at all — which is exactly the part #2861 fixes.
|
|
55
|
+
expect(content).toContain('mapArrayLazy(')
|
|
56
|
+
expect(content).toContain('indexDriven: true')
|
|
57
|
+
expect(content).toMatch(/applyItem: \(__e\) => \{[\s\S]*?const i = __e\.index/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('class={i % 2 === 0 ? … : …} wires a createEffect on the eager path', () => {
|
|
61
|
+
const content = clientJsFor(`
|
|
62
|
+
'use client'
|
|
63
|
+
import { createSignal } from '@barefootjs/client'
|
|
64
|
+
export function Repro() {
|
|
65
|
+
const [items] = createSignal([{ id: 'a' }, { id: 'b' }])
|
|
66
|
+
return (
|
|
67
|
+
<ul>
|
|
68
|
+
{items().map((item, i) => (
|
|
69
|
+
<li key={item.id} class={i % 2 === 0 ? 'even' : 'odd'} ref={(el: HTMLElement | null) => {}}>{item.id}</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
`)
|
|
75
|
+
|
|
76
|
+
// A ref forces the eager `mapArray` path (no lazy row graph for rows
|
|
77
|
+
// that own an imperative ref) — the class binding must still be wired
|
|
78
|
+
// through a per-item `createEffect` reading the index ACCESSOR (#2859).
|
|
79
|
+
// The expression is read into `__x` first (#2869's dedup-guard shape,
|
|
80
|
+
// `emitDedupedAttrUpdate`) before the write reads it back as `__v`.
|
|
81
|
+
expect(content).toContain('mapArray(')
|
|
82
|
+
expect(content).toMatch(
|
|
83
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = `\$\{i\(\) % 2 === 0 \? 'even' : 'odd'\}`[\s\S]*?const __v = __x;/
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('an index-only conditional ({i % 2 === 0 ? <b/> : <em/>}) reconciles reactively on the lazy path', () => {
|
|
88
|
+
const content = clientJsFor(`
|
|
89
|
+
'use client'
|
|
90
|
+
import { createSignal } from '@barefootjs/client'
|
|
91
|
+
export function Repro() {
|
|
92
|
+
const [items] = createSignal([{ id: 'a' }, { id: 'b' }])
|
|
93
|
+
return (
|
|
94
|
+
<ul>
|
|
95
|
+
{items().map((item, i) => (
|
|
96
|
+
<li key={item.id}>{i % 2 === 0 ? <b>even</b> : <em>odd</em>}</li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
`)
|
|
102
|
+
|
|
103
|
+
// Before #2861 this pre-gate (`collectLoopChildConditionals`'s
|
|
104
|
+
// `refsAnyBindingViaFreeIds`) never even reached `classifyReactivity` —
|
|
105
|
+
// an index-only condition died at the AST-`reactive`-flag short-circuit
|
|
106
|
+
// and baked into `createRow` only, once, forever.
|
|
107
|
+
expect(content).toContain('mapArrayLazy(')
|
|
108
|
+
expect(content).toMatch(/applyItem: \(__e\) => \{[\s\S]*?const i = __e\.index/)
|
|
109
|
+
expect(content).toContain('!!(i % 2 === 0)')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('nested loop: referencing an OUTER loop\'s own index (#2861)', () => {
|
|
114
|
+
test('composite path: inner row text reads the outer index through its accessor', () => {
|
|
115
|
+
const content = clientJsFor(`
|
|
116
|
+
'use client'
|
|
117
|
+
import { createSignal } from '@barefootjs/client'
|
|
118
|
+
interface Item { id: string; label: string }
|
|
119
|
+
interface Group { id: string; items: Item[] }
|
|
120
|
+
export function Repro() {
|
|
121
|
+
const [groups] = createSignal<Group[]>([])
|
|
122
|
+
return (
|
|
123
|
+
<div>
|
|
124
|
+
{groups().map((g, gi) => (
|
|
125
|
+
<div key={g.id}>
|
|
126
|
+
{g.items.map((it) => (
|
|
127
|
+
<span key={it.id}>{gi}-{it.label}</span>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
`)
|
|
135
|
+
|
|
136
|
+
// The inner mapArray's own renderItem has no `gi` param at all — the
|
|
137
|
+
// outer accessor must be read directly by name, exactly as `item()`
|
|
138
|
+
// reads the outer ITEM in the sibling case #2865 already covers.
|
|
139
|
+
expect(content).toMatch(
|
|
140
|
+
/mapArray\(\(\) => g\(\)\.items \|\| \[\], .*?, \(it, __innerIdx\d+_\d+, __existing\) => \{[\s\S]*?createEffect\(\(\) => \{ __bfw_s0\('s0', String\(gi\(\)\)\) \}\)/
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('composite path: inner row class attr reads the outer index through its accessor', () => {
|
|
145
|
+
const content = clientJsFor(`
|
|
146
|
+
'use client'
|
|
147
|
+
import { createSignal } from '@barefootjs/client'
|
|
148
|
+
interface Item { id: string }
|
|
149
|
+
interface Group { id: string; items: Item[] }
|
|
150
|
+
export function Repro() {
|
|
151
|
+
const [groups] = createSignal<Group[]>([])
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
{groups().map((g, gi) => (
|
|
155
|
+
<div key={g.id}>
|
|
156
|
+
{g.items.map((it) => (
|
|
157
|
+
<span key={it.id} class={gi % 2 === 0 ? 'even' : 'odd'}>{it.id}</span>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
`)
|
|
165
|
+
|
|
166
|
+
// The expression is read into `__x` first (#2869's dedup-guard shape,
|
|
167
|
+
// `emitDedupedAttrUpdate`) before the write reads it back as `__v`.
|
|
168
|
+
expect(content).toMatch(
|
|
169
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = `\$\{gi\(\) % 2 === 0 \? 'even' : 'odd'\}`[\s\S]*?const __v = __x;/
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('conditional-branch-arm path: a loop nested inside a branch reads the OUTER (pre-branch) loop\'s index', () => {
|
|
174
|
+
const content = clientJsFor(`
|
|
175
|
+
'use client'
|
|
176
|
+
import { createSignal } from '@barefootjs/client'
|
|
177
|
+
interface Item { id: string; text: string }
|
|
178
|
+
interface Group { id: string; flag: boolean; items: Item[] }
|
|
179
|
+
export function Repro() {
|
|
180
|
+
const [outer] = createSignal<Group[]>([])
|
|
181
|
+
return (
|
|
182
|
+
<div>
|
|
183
|
+
{outer().map((o, oi) => (
|
|
184
|
+
<div key={o.id}>
|
|
185
|
+
{o.flag ? (
|
|
186
|
+
<div>
|
|
187
|
+
{o.items.map((item) => (
|
|
188
|
+
<span key={item.id}>{oi}: {item.text}</span>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
) : null}
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
`)
|
|
198
|
+
|
|
199
|
+
// Before #2861, `collectInnerLoops` (called from `summarizeLoopChildBranch`
|
|
200
|
+
// for a loop nested inside a conditional arm) only ever built a scope
|
|
201
|
+
// from the INNER loop's own param/index — a row referencing the loop
|
|
202
|
+
// ONE LEVEL UP (the one whose conditional this branch belongs to) was
|
|
203
|
+
// classified 'none' and baked once into the branch's initial template.
|
|
204
|
+
expect(content).toMatch(
|
|
205
|
+
/mapArray\(\(\) => o\(\)\.items \|\| \[\], .*?, \(item, __bidxbr_\d+, __existing\) => \{[\s\S]*?createEffect\(\(\) => \{ __bfw_s1\('s1', String\(oi\(\)\)\) \}\)/
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -71,9 +71,11 @@ describe('#1665 — keyless whole-item conditional emits valid JS (BF023)', () =
|
|
|
71
71
|
const { js, errorCodes } = clientJsFor('sel() === t.id && <li>{t.id}</li>', /* withKey */ false)
|
|
72
72
|
expect(errorCodes).toContain(ErrorCodes.MISSING_KEY_IN_LIST)
|
|
73
73
|
// No empty template-literal interpolation, and the anchor falls back to the
|
|
74
|
-
// iteration index so the comment value is well-formed.
|
|
74
|
+
// iteration index so the comment value is well-formed. `__idx` is bound to
|
|
75
|
+
// an index ACCESSOR at runtime (#2859), so the fallback calls it like
|
|
76
|
+
// every other reference to the loop's index parameter.
|
|
75
77
|
expect(js).not.toContain('${}')
|
|
76
|
-
expect(js).toContain('bf-loop-i:${__idx}')
|
|
78
|
+
expect(js).toContain('bf-loop-i:${__idx()}')
|
|
77
79
|
// The whole client module must parse.
|
|
78
80
|
const body = js.replace(/^import[^\n]*\n/gm, '').replace(/^export /gm, '')
|
|
79
81
|
expect(() => new Function(body)).not.toThrow()
|
|
@@ -60,7 +60,7 @@ describe('.map() array-builder body — JS-runtime verbatim (Stage 3 / D4)', ()
|
|
|
60
60
|
// Byte-level parity is pinned by the map-array-builder-escaping fixture.
|
|
61
61
|
const r = compile(arrayBuilder, false)
|
|
62
62
|
const cj = r.files.find(f => f.type === 'clientJs')!.content
|
|
63
|
-
expect(cj).toMatch(/out\.push\(`<td>\$\{escapeText\(
|
|
63
|
+
expect(cj).toMatch(/out\.push\(`<td>\$\{escapeText\(c\)\}<\/td>`\)/)
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
test('the element-array child {out} is joined into the row', () => {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2867 / #2830: `shouldAutoDeferReactiveBrand` (jsx-to-ir.ts) used to call
|
|
3
|
+
* `getJS()` on the whole candidate expression to regex-test it for a native
|
|
4
|
+
* signal/memo call. For a `.map()` loop or a ternary/`&&` with JSX arms, that
|
|
5
|
+
* expression IS JSX-bearing — and whenever the TypeChecker was active and
|
|
6
|
+
* resolved a `Reactive<T>` brand anywhere in the expression, `getJS()`'s
|
|
7
|
+
* trust-boundary assertion (armed by `map-body-no-silent-divergence.test.ts`)
|
|
8
|
+
* threw, uncaught, on `signal-array-builder` / `flatmap-expression-body-signal-array`.
|
|
9
|
+
*
|
|
10
|
+
* That harness's own `assess()` compiles via a bare relative path (`T.tsx`)
|
|
11
|
+
* with no injected `ts.Program`, so whether the checker actually activates —
|
|
12
|
+
* and thus whether this reproduces — silently depended on the process's cwd
|
|
13
|
+
* and whether `@barefootjs/client`'s `dist` happened to be built, which is
|
|
14
|
+
* exactly why the failure looked like an environment-dependent flake (#2867)
|
|
15
|
+
* rather than the deterministic compiler bug it is. These tests pin it with
|
|
16
|
+
* an explicitly injected `ts.Program` (mirroring `auto-defer-brand.test.ts`)
|
|
17
|
+
* so the repro — and the fix — hold regardless of cwd or build state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
|
21
|
+
import ts from 'typescript'
|
|
22
|
+
import path from 'path'
|
|
23
|
+
import { compileJSX } from '../compiler'
|
|
24
|
+
import { TestAdapter } from '../adapters/test-adapter'
|
|
25
|
+
|
|
26
|
+
let prevAssertEnv: string | undefined
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
prevAssertEnv = process.env.BF_ASSERT_NO_JSX_IN_GETJS
|
|
29
|
+
process.env.BF_ASSERT_NO_JSX_IN_GETJS = '1'
|
|
30
|
+
})
|
|
31
|
+
afterAll(() => {
|
|
32
|
+
if (prevAssertEnv === undefined) delete process.env.BF_ASSERT_NO_JSX_IN_GETJS
|
|
33
|
+
else process.env.BF_ASSERT_NO_JSX_IN_GETJS = prevAssertEnv
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
interface Compiled {
|
|
37
|
+
errors: string[]
|
|
38
|
+
hasError: boolean
|
|
39
|
+
clientJs: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compile `source` with a real TypeChecker that resolves `@barefootjs/client`
|
|
44
|
+
* (found deterministically via `packages/jsx/node_modules`, since the virtual
|
|
45
|
+
* file lives under this file's own directory — no dependency on `process.cwd()`).
|
|
46
|
+
*/
|
|
47
|
+
function compileWithChecker(source: string): Compiled {
|
|
48
|
+
const componentPath = path.join(path.resolve(__dirname), '_map-body-brand-component.tsx')
|
|
49
|
+
const virtualFiles = new Map<string, string>([[componentPath, source]])
|
|
50
|
+
|
|
51
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
52
|
+
target: ts.ScriptTarget.Latest,
|
|
53
|
+
module: ts.ModuleKind.ESNext,
|
|
54
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
55
|
+
jsx: ts.JsxEmit.ReactJSX,
|
|
56
|
+
strict: true,
|
|
57
|
+
skipLibCheck: true,
|
|
58
|
+
noEmit: true,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaultHost = ts.createCompilerHost(compilerOptions)
|
|
62
|
+
const host: ts.CompilerHost = {
|
|
63
|
+
...defaultHost,
|
|
64
|
+
getSourceFile(fileName, languageVersion) {
|
|
65
|
+
const resolved = path.resolve(fileName)
|
|
66
|
+
const content = virtualFiles.get(resolved)
|
|
67
|
+
if (content !== undefined) {
|
|
68
|
+
return ts.createSourceFile(fileName, content, languageVersion, true, ts.ScriptKind.TSX)
|
|
69
|
+
}
|
|
70
|
+
return defaultHost.getSourceFile(fileName, languageVersion)
|
|
71
|
+
},
|
|
72
|
+
fileExists(fileName) {
|
|
73
|
+
return virtualFiles.has(path.resolve(fileName)) || defaultHost.fileExists(fileName)
|
|
74
|
+
},
|
|
75
|
+
readFile(fileName) {
|
|
76
|
+
return virtualFiles.get(path.resolve(fileName)) ?? defaultHost.readFile(fileName)
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const program = ts.createProgram([componentPath], compilerOptions, host)
|
|
81
|
+
const adapter = new TestAdapter()
|
|
82
|
+
const r = compileJSX(source, componentPath, { adapter, program })
|
|
83
|
+
const clientJs = r.files.find((f) => f.type === 'clientJs')?.content ?? ''
|
|
84
|
+
return {
|
|
85
|
+
errors: r.errors.map((e) => `[${e.code}] ${e.message}`),
|
|
86
|
+
hasError: r.errors.some((e) => e.severity === 'error'),
|
|
87
|
+
clientJs,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertSound(clientJs: string): void {
|
|
92
|
+
const sf = ts.createSourceFile('bundle.js', clientJs, ts.ScriptTarget.ES2022, false, ts.ScriptKind.JS)
|
|
93
|
+
const parseErrors = (sf as unknown as { parseDiagnostics: unknown[] }).parseDiagnostics.length
|
|
94
|
+
expect(parseErrors).toBe(0)
|
|
95
|
+
expect(clientJs).not.toMatch(/__BF_JSX_\d+__/)
|
|
96
|
+
expect(clientJs).not.toMatch(/\bpush\(</)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('shouldAutoDeferReactiveBrand does not stringify JSX-bearing expressions (#2867/#2830)', () => {
|
|
100
|
+
test('signal-sourced .map() array builder compiles clean under an active checker', () => {
|
|
101
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
102
|
+
'use client'
|
|
103
|
+
import { createSignal } from '@barefootjs/client'
|
|
104
|
+
function T() {
|
|
105
|
+
const [rows] = createSignal([{ id: '1', cells: ['a'] }])
|
|
106
|
+
return <table><tbody>{rows().map((r) => {
|
|
107
|
+
const out = []
|
|
108
|
+
for (const c of r.cells) out.push(<td>{c}</td>)
|
|
109
|
+
return <tr key={r.id}>{out}</tr>
|
|
110
|
+
})}</tbody></table>
|
|
111
|
+
}
|
|
112
|
+
export { T }`)
|
|
113
|
+
expect(hasError).toBe(false)
|
|
114
|
+
assertSound(clientJs)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('signal-sourced .flatMap() with an expression-body inner .map() compiles clean under an active checker', () => {
|
|
118
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
119
|
+
'use client'
|
|
120
|
+
import { createSignal } from '@barefootjs/client'
|
|
121
|
+
function T() {
|
|
122
|
+
const [items] = createSignal([{ id: '1', tags: ['a'] }])
|
|
123
|
+
return <ul>{items().flatMap((it) => it.tags.map((t) => <li key={t}>{t}</li>))}</ul>
|
|
124
|
+
}
|
|
125
|
+
export { T }`)
|
|
126
|
+
expect(hasError).toBe(false)
|
|
127
|
+
assertSound(clientJs)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('memo-sourced .map() array builder also compiles clean (not just signals)', () => {
|
|
131
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
132
|
+
'use client'
|
|
133
|
+
import { createSignal, createMemo } from '@barefootjs/client'
|
|
134
|
+
function T() {
|
|
135
|
+
const [rows] = createSignal([{ id: '1', cells: ['a'] }])
|
|
136
|
+
const upper = createMemo(() => rows())
|
|
137
|
+
return <table><tbody>{upper().map((r) => {
|
|
138
|
+
const out = []
|
|
139
|
+
for (const c of r.cells) out.push(<td>{c}</td>)
|
|
140
|
+
return <tr key={r.id}>{out}</tr>
|
|
141
|
+
})}</tbody></table>
|
|
142
|
+
}
|
|
143
|
+
export { T }`)
|
|
144
|
+
expect(hasError).toBe(false)
|
|
145
|
+
assertSound(clientJs)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('signal-conditioned ternary with JSX on both arms, no props destructuring (BF043) to mask the assertion', () => {
|
|
149
|
+
// No destructured props here (which would emit a BF043 warning and, pre-
|
|
150
|
+
// fix, incidentally have silently disarmed the trust-boundary assertion
|
|
151
|
+
// for this shape). This pins the conditional-arm path directly.
|
|
152
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
153
|
+
'use client'
|
|
154
|
+
import { createSignal } from '@barefootjs/client'
|
|
155
|
+
function T() {
|
|
156
|
+
const [show] = createSignal(true)
|
|
157
|
+
return <div>{show() ? <b>yes</b> : <em>no</em>}</div>
|
|
158
|
+
}
|
|
159
|
+
export { T }`)
|
|
160
|
+
expect(hasError).toBe(false)
|
|
161
|
+
assertSound(clientJs)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('a component also importing @barefootjs/client but with no .map() and no reactive JSX branch is unaffected', () => {
|
|
165
|
+
const { hasError, clientJs } = compileWithChecker(`
|
|
166
|
+
'use client'
|
|
167
|
+
import { createSignal } from '@barefootjs/client'
|
|
168
|
+
function T() {
|
|
169
|
+
const [count, setCount] = createSignal(0)
|
|
170
|
+
return <button onClick={() => setCount((c) => c + 1)}>{count()}</button>
|
|
171
|
+
}
|
|
172
|
+
export { T }`)
|
|
173
|
+
expect(hasError).toBe(false)
|
|
174
|
+
assertSound(clientJs)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -63,7 +63,11 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
63
63
|
// The renderItem body binds the alias before using it.
|
|
64
64
|
expect(content).toContain('const i = __innerIdx')
|
|
65
65
|
const aliasIdx = content.indexOf('const i = __innerIdx')
|
|
66
|
-
|
|
66
|
+
// `i` is bound to an index ACCESSOR at runtime (#2859) — `mapArray` hands
|
|
67
|
+
// every renderItem an index accessor (mirroring the item accessor) and
|
|
68
|
+
// pushes the row's current position through it on reorder, so `i()` reads
|
|
69
|
+
// the live value instead of a plain number frozen at row creation.
|
|
70
|
+
const setAttrIdx = content.indexOf(`setAttribute('data-key-1', String(i()))`)
|
|
67
71
|
expect(aliasIdx).toBeGreaterThanOrEqual(0)
|
|
68
72
|
expect(setAttrIdx).toBeGreaterThan(aliasIdx)
|
|
69
73
|
// The alias must land before the clone IIFE too — the cloned template's
|
|
@@ -104,6 +108,14 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
104
108
|
// *after* the alias, inside the inner renderItem's own clone template.
|
|
105
109
|
const templateIdx = content.indexOf('escapeText(i)', aliasIdx)
|
|
106
110
|
expect(templateIdx).toBeGreaterThan(aliasIdx)
|
|
111
|
+
// #2861: a bare `{i}` — no signal, no function call anywhere in the
|
|
112
|
+
// expression — used to be classified fully static by Phase 2 and never
|
|
113
|
+
// get a `createEffect` at all, so it stayed frozen at whatever value it
|
|
114
|
+
// had when the row was first created. `classifyReactivity`'s new
|
|
115
|
+
// `loop-index` source fixes this: the index is wired through the same
|
|
116
|
+
// accessor `mapArray` already hands every row (#2859/#2860), and the
|
|
117
|
+
// slot now gets a real update effect.
|
|
118
|
+
expect(content).toContain(`createEffect(() => { __bfw_s0('s0', String(i())) })`)
|
|
107
119
|
})
|
|
108
120
|
|
|
109
121
|
test('index referenced in a reactive attribute expression is bound', () => {
|
|
@@ -130,6 +142,17 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
130
142
|
|
|
131
143
|
expect(content).toContain('const i = __innerIdx')
|
|
132
144
|
expect(content).toContain(`i % 2 === 0 ? 'even' : 'odd'`)
|
|
145
|
+
// #2861: `class={i % 2 === 0 ? …}` has no signal read and no function
|
|
146
|
+
// call — before `classifyReactivity`'s `loop-index` source existed this
|
|
147
|
+
// was classified fully static and never got a `createEffect`, so the
|
|
148
|
+
// class stayed frozen at whichever value the row was created with.
|
|
149
|
+
// The expression is read into `__x` first (#2869's dedup-guard shape,
|
|
150
|
+
// `emitDedupedAttrUpdate`) before the actual attribute write reads it
|
|
151
|
+
// back as `__v` — assert both halves rather than the old direct
|
|
152
|
+
// `const __v = <expr>` shape #2869 replaced.
|
|
153
|
+
expect(content).toMatch(
|
|
154
|
+
/createEffect\(\(\) => \{[\s\S]*?const __x = `\$\{i\(\) % 2 === 0 \? 'even' : 'odd'\}`[\s\S]*?const __v = __x;/
|
|
155
|
+
)
|
|
133
156
|
})
|
|
134
157
|
|
|
135
158
|
test('index referenced only in an event handler inside the inner loop is bound', () => {
|
|
@@ -156,7 +179,12 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
156
179
|
`)
|
|
157
180
|
|
|
158
181
|
expect(content).toContain('const i = __innerIdx')
|
|
159
|
-
|
|
182
|
+
// A direct (non-delegated) listener on a nested-loop row reads the row's
|
|
183
|
+
// LIVE index accessor (#2859) — unlike the top-level delegated click
|
|
184
|
+
// dispatcher (`event-delegation-index-param.test.ts`), this handler is
|
|
185
|
+
// attached once at row creation, so without this it would keep firing
|
|
186
|
+
// with the index the row was first created at after a same-key reorder.
|
|
187
|
+
expect(content).toContain('setClicked(i())')
|
|
160
188
|
})
|
|
161
189
|
|
|
162
190
|
test('byte-stability: index declared but never referenced emits no alias', () => {
|
|
@@ -214,10 +242,13 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
214
242
|
expect(content).not.toContain('const id = __innerIdx')
|
|
215
243
|
})
|
|
216
244
|
|
|
217
|
-
test('
|
|
218
|
-
// Sibling of the reactive-path test above,
|
|
219
|
-
//
|
|
220
|
-
//
|
|
245
|
+
test('inner array with no outer-item dependency still binds the index alias (#2865: no more static forEach)', () => {
|
|
246
|
+
// Sibling of the reactive-path test above. Before #2865, an inner loop
|
|
247
|
+
// whose array didn't reference the outer loop param was routed to
|
|
248
|
+
// `emitStatic` in `stringify/inner-loop.ts` — a hydration-only
|
|
249
|
+
// `forEach` that never wired reactivity for anything inside the row.
|
|
250
|
+
// That fork is gone: every inner loop now gets the full `mapArray`
|
|
251
|
+
// emission regardless of what its array depends on.
|
|
221
252
|
const content = clientJsFor(`
|
|
222
253
|
'use client'
|
|
223
254
|
import { createSignal } from '@barefootjs/client'
|
|
@@ -242,7 +273,7 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
242
273
|
}
|
|
243
274
|
`)
|
|
244
275
|
|
|
245
|
-
expect(content).toMatch(/
|
|
276
|
+
expect(content).toMatch(/mapArray\(\(\) => SHARED \|\| \[\], .*?, \(cell, __innerIdx[^)]+, __existing\) => \{[\s\S]*?const\s+i\s*=\s*__innerIdx/)
|
|
246
277
|
})
|
|
247
278
|
|
|
248
279
|
test('conditional-branch-arm inner loop (loop -> conditional -> inner loop) binds the index alias', () => {
|
|
@@ -278,7 +309,9 @@ describe('nested .map() index param referenced in key/text/attr (#2218)', () =>
|
|
|
278
309
|
expect(content).toContain('(item, i) => String(i)')
|
|
279
310
|
expect(content).toContain('const i = __bidx')
|
|
280
311
|
const aliasIdx = content.indexOf('const i = __bidx')
|
|
281
|
-
|
|
312
|
+
// `i` reads the live index accessor (#2859) — see the sibling assertion
|
|
313
|
+
// in the 'key={i}' test above.
|
|
314
|
+
const setAttrIdx = content.indexOf(`setAttribute('data-key-1', String(i()))`)
|
|
282
315
|
expect(aliasIdx).toBeGreaterThanOrEqual(0)
|
|
283
316
|
expect(setAttrIdx).toBeGreaterThan(aliasIdx)
|
|
284
317
|
})
|