@barefootjs/jsx 0.18.4 → 0.18.7
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 +77 -0
- package/dist/adapters/dangerous-inner-html.d.ts.map +1 -0
- package/dist/adapters/loop-bound-names.d.ts +22 -0
- package/dist/adapters/loop-bound-names.d.ts.map +1 -0
- package/dist/adapters/parsed-expr-emitter.d.ts +2 -2
- package/dist/adapters/parsed-expr-emitter.d.ts.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/expression-parser.d.ts +2 -1
- package/dist/expression-parser.d.ts.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1108 -307
- package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-inner-loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/build-loop-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/event-delegation.d.ts +10 -0
- package/dist/ir-to-client-js/control-flow/plan/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +9 -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 +9 -0
- package/dist/ir-to-client-js/control-flow/plan/loop.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/shared.d.ts +45 -3
- package/dist/ir-to-client-js/control-flow/shared.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/event-delegation.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/loop.d.ts +1 -0
- 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 +10 -0
- package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts +38 -0
- package/dist/ir-to-client-js/control-flow/stringify/skeleton-paths.d.ts.map +1 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts +19 -0
- package/dist/ir-to-client-js/control-flow/stringify/template-parse.d.ts.map +1 -1
- package/dist/ir-to-client-js/csr-substitute.d.ts +24 -0
- package/dist/ir-to-client-js/csr-substitute.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +42 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts +20 -1
- package/dist/ir-to-client-js/plan/static-array-child-init.d.ts.map +1 -1
- package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
- package/dist/ir-to-client-js/stringify/static-array-child-init.d.ts +1 -1
- package/dist/ir-to-client-js/types.d.ts +30 -0
- package/dist/ir-to-client-js/types.d.ts.map +1 -1
- package/dist/ir-to-client-js/utils.d.ts +25 -0
- package/dist/ir-to-client-js/utils.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/signal-init-eval.d.ts +82 -0
- package/dist/signal-init-eval.d.ts.map +1 -0
- package/dist/static-literal.d.ts +45 -0
- package/dist/static-literal.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/doc-examples.test.ts.snap +19 -13
- package/src/__tests__/create-selector.test.ts +110 -0
- package/src/__tests__/csr-template-loop-shadowing.test.ts +214 -0
- package/src/__tests__/dangerous-inner-html-resolver.test.ts +197 -0
- package/src/__tests__/event-delegation-index-param.test.ts +130 -0
- package/src/__tests__/expression-parser.test.ts +38 -0
- package/src/__tests__/inner-loop-svg-namespace.test.ts +245 -0
- package/src/__tests__/ir-walker.test.ts +1 -0
- package/src/__tests__/loop-bound-names.test.ts +102 -0
- package/src/__tests__/loop-plan-classification.test.ts +2 -0
- package/src/__tests__/map-function-reference.test.ts +303 -0
- package/src/__tests__/materialize-getter-calls.test.ts +1 -0
- package/src/__tests__/nested-loop-index-param.test.ts +285 -0
- package/src/__tests__/signal-init-eval.test.ts +138 -0
- package/src/__tests__/static-array-inner-loop-index-param.test.ts +180 -0
- package/src/__tests__/static-literal.test.ts +126 -0
- package/src/__tests__/string-concat-identifier.test.ts +55 -0
- package/src/adapters/dangerous-inner-html.ts +186 -0
- package/src/adapters/loop-bound-names.ts +80 -0
- package/src/adapters/parsed-expr-emitter.ts +19 -1
- package/src/analyzer.ts +6 -0
- package/src/expression-parser.ts +59 -25
- package/src/index.ts +19 -3
- package/src/ir-to-client-js/collect-elements.ts +18 -3
- package/src/ir-to-client-js/control-flow/plan/build-event-delegation.ts +6 -0
- package/src/ir-to-client-js/control-flow/plan/build-inner-loop.ts +21 -4
- package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +13 -1
- package/src/ir-to-client-js/control-flow/plan/build-loop.ts +1 -0
- package/src/ir-to-client-js/control-flow/plan/event-delegation.ts +10 -0
- package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +9 -0
- package/src/ir-to-client-js/control-flow/plan/loop.ts +9 -0
- package/src/ir-to-client-js/control-flow/shared.ts +103 -6
- package/src/ir-to-client-js/control-flow/stringify/event-delegation.ts +31 -7
- package/src/ir-to-client-js/control-flow/stringify/inner-loop.ts +11 -2
- package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +5 -0
- package/src/ir-to-client-js/control-flow/stringify/loop.ts +53 -10
- package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +25 -4
- package/src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts +70 -0
- package/src/ir-to-client-js/control-flow/stringify/template-parse.ts +25 -1
- package/src/ir-to-client-js/csr-substitute.ts +54 -1
- package/src/ir-to-client-js/html-template.ts +292 -13
- package/src/ir-to-client-js/imports.ts +1 -0
- package/src/ir-to-client-js/plan/build-static-array-child-init.ts +12 -1
- package/src/ir-to-client-js/plan/static-array-child-init.ts +20 -1
- package/src/ir-to-client-js/reactivity.ts +6 -0
- package/src/ir-to-client-js/stringify/static-array-child-init.ts +11 -6
- package/src/ir-to-client-js/types.ts +31 -0
- package/src/ir-to-client-js/utils.ts +34 -1
- package/src/jsx-to-ir.ts +322 -48
- package/src/signal-init-eval.ts +165 -0
- package/src/static-literal.ts +128 -0
- package/src/types.ts +68 -0
|
@@ -70,6 +70,43 @@ describe('expression-parser', () => {
|
|
|
70
70
|
expect(result.kind).toBe('member')
|
|
71
71
|
if (result.kind === 'member') {
|
|
72
72
|
expect(result.property).toBe('name')
|
|
73
|
+
expect(result.optional).toBe(false)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// #2168 optional-chaining-prop: `optional` must reflect the source `?.`
|
|
78
|
+
// token exactly — set on the one written hop, `false` everywhere else
|
|
79
|
+
// (a plain `.`/`[]` access, including a literal-index computed access).
|
|
80
|
+
test('parses optional chaining (?.) into `member.optional`', () => {
|
|
81
|
+
const dotResult = parseExpression('user?.name')
|
|
82
|
+
expect(dotResult.kind).toBe('member')
|
|
83
|
+
if (dotResult.kind === 'member') {
|
|
84
|
+
expect(dotResult.property).toBe('name')
|
|
85
|
+
expect(dotResult.computed).toBe(false)
|
|
86
|
+
expect(dotResult.optional).toBe(true)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const stringKeyResult = parseExpression("obj?.['key']")
|
|
90
|
+
expect(stringKeyResult.kind).toBe('member')
|
|
91
|
+
if (stringKeyResult.kind === 'member') {
|
|
92
|
+
expect(stringKeyResult.property).toBe('key')
|
|
93
|
+
expect(stringKeyResult.computed).toBe(true)
|
|
94
|
+
expect(stringKeyResult.optional).toBe(true)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const numericIndexResult = parseExpression('arr?.[0]')
|
|
98
|
+
expect(numericIndexResult.kind).toBe('member')
|
|
99
|
+
if (numericIndexResult.kind === 'member') {
|
|
100
|
+
expect(numericIndexResult.property).toBe('0')
|
|
101
|
+
expect(numericIndexResult.computed).toBe(true)
|
|
102
|
+
expect(numericIndexResult.optional).toBe(true)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Non-optional literal-index access stays `optional: false`.
|
|
106
|
+
const plainIndexResult = parseExpression('arr[0]')
|
|
107
|
+
expect(plainIndexResult.kind).toBe('member')
|
|
108
|
+
if (plainIndexResult.kind === 'member') {
|
|
109
|
+
expect(plainIndexResult.optional).toBe(false)
|
|
73
110
|
}
|
|
74
111
|
})
|
|
75
112
|
|
|
@@ -1230,6 +1267,7 @@ describe('expression-parser', () => {
|
|
|
1230
1267
|
object: { kind: 'identifier', name: 'x' },
|
|
1231
1268
|
property: 'done',
|
|
1232
1269
|
computed: false,
|
|
1270
|
+
optional: false,
|
|
1233
1271
|
})
|
|
1234
1272
|
})
|
|
1235
1273
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVG-rooted loop item templates cloned via `template.innerHTML` (#2219).
|
|
3
|
+
*
|
|
4
|
+
* `template.innerHTML` parses in the HTML namespace, so an inner reactive
|
|
5
|
+
* loop whose item root is an SVG element (`<line>`, `<circle>`, ...) cloned
|
|
6
|
+
* as an `HTMLUnknownElement` — present in the DOM but never drawn by the
|
|
7
|
+
* SVG renderer, with no error. The top-level loop path has wrapped these
|
|
8
|
+
* templates in a synthetic `<svg>` since #135/#1088 (`templateRootIsSvg` /
|
|
9
|
+
* `emitTemplateCloneInline` in `stringify/template-parse.ts`) and the
|
|
10
|
+
* branch-arm path since its plan-ification, but the inner-loop reactive
|
|
11
|
+
* clone (`stringify/inner-loop.ts`) and the static-loop CSR materialize
|
|
12
|
+
* clones (#1247, `stringify/loop.ts`) parsed bare and stayed in the wrong
|
|
13
|
+
* namespace.
|
|
14
|
+
*
|
|
15
|
+
* The fix mirrors the established wrap: parse `<svg>${template}</svg>` and
|
|
16
|
+
* descend one extra level (`.firstElementChild.firstElementChild`).
|
|
17
|
+
* HTML-rooted templates keep byte-identical output.
|
|
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('inner reactive loop with an SVG element root (#2219)', () => {
|
|
35
|
+
test('SVG-rooted inner item clones inside a synthetic <svg> wrap', () => {
|
|
36
|
+
const content = clientJsFor(`
|
|
37
|
+
'use client'
|
|
38
|
+
import { createSignal } from '@barefootjs/client'
|
|
39
|
+
interface Sheet { id: number; ticks: number[] }
|
|
40
|
+
export function Repro() {
|
|
41
|
+
const [sheets] = createSignal<Sheet[]>([])
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
{sheets().map((s) => (
|
|
45
|
+
<svg key={s.id} viewBox="0 0 100 100">
|
|
46
|
+
{s.ticks.map((y) => (
|
|
47
|
+
<line key={y} x1="0" x2="100" y1={y} y2={y} stroke="black" />
|
|
48
|
+
))}
|
|
49
|
+
</svg>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
`)
|
|
55
|
+
|
|
56
|
+
// The inner renderItem clone parses inside `<svg>...</svg>` and descends
|
|
57
|
+
// one extra level to reach the real `<line>` root.
|
|
58
|
+
expect(content).toMatch(/__t\.innerHTML = `<svg><line /)
|
|
59
|
+
expect(content).toContain('return __t.content.firstElementChild.firstElementChild.cloneNode(true)')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('HTML-rooted inner item keeps the bare clone byte-identical', () => {
|
|
63
|
+
const content = clientJsFor(`
|
|
64
|
+
'use client'
|
|
65
|
+
import { createSignal } from '@barefootjs/client'
|
|
66
|
+
interface Group { id: number; items: string[] }
|
|
67
|
+
export function Repro() {
|
|
68
|
+
const [groups] = createSignal<Group[]>([])
|
|
69
|
+
return (
|
|
70
|
+
<ul>
|
|
71
|
+
{groups().map((g) => (
|
|
72
|
+
<li key={g.id}>
|
|
73
|
+
{g.items.map((t) => (
|
|
74
|
+
<span key={t}>{t}</span>
|
|
75
|
+
))}
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
`)
|
|
82
|
+
|
|
83
|
+
expect(content).toMatch(/__t\.innerHTML = `<span /)
|
|
84
|
+
expect(content).toContain('return __t.content.firstElementChild.cloneNode(true)')
|
|
85
|
+
// No synthetic wrap anywhere in the inner clone.
|
|
86
|
+
expect(content).not.toContain('`<svg><span')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('conditional inner body whose branches are all SVG wraps too (#1088 shape)', () => {
|
|
90
|
+
const content = clientJsFor(`
|
|
91
|
+
'use client'
|
|
92
|
+
import { createSignal } from '@barefootjs/client'
|
|
93
|
+
interface Sheet { id: number; ticks: number[] }
|
|
94
|
+
export function Repro() {
|
|
95
|
+
const [sheets] = createSignal<Sheet[]>([])
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
{sheets().map((s) => (
|
|
99
|
+
<svg key={s.id}>
|
|
100
|
+
{s.ticks.map((y) => (
|
|
101
|
+
y > 50
|
|
102
|
+
? <line key={y} y1={y} y2={y} />
|
|
103
|
+
: <circle key={y} cy={y} r="1" />
|
|
104
|
+
))}
|
|
105
|
+
</svg>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
`)
|
|
111
|
+
|
|
112
|
+
// Both branches are SVG roots, so the interpolated conditional template
|
|
113
|
+
// still gets the wrap (templateRootIsSvg recurses into the branches).
|
|
114
|
+
expect(content).toMatch(/__t\.innerHTML = `<svg>\$\{/)
|
|
115
|
+
expect(content).toContain('return __t.content.firstElementChild.firstElementChild.cloneNode(true)')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('reactive multi-root fragment with an <svg>-container first root (#2233 review)', () => {
|
|
120
|
+
test('emitMultiRootTemplateCloneLines skips the wrap for <svg>-first fragments', () => {
|
|
121
|
+
const content = clientJsFor(`
|
|
122
|
+
'use client'
|
|
123
|
+
import { createSignal } from '@barefootjs/client'
|
|
124
|
+
interface Chart { id: number; r: number }
|
|
125
|
+
export function Repro() {
|
|
126
|
+
const [charts] = createSignal<Chart[]>([])
|
|
127
|
+
return (
|
|
128
|
+
<div>
|
|
129
|
+
{charts().map((c) => (
|
|
130
|
+
<>
|
|
131
|
+
<svg key={c.id} viewBox="0 0 10 10"><circle r={c.r} /></svg>
|
|
132
|
+
<span>{c.id}</span>
|
|
133
|
+
</>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
`)
|
|
139
|
+
|
|
140
|
+
// The multi-root clone parses the fragment bare — no synthetic wrap —
|
|
141
|
+
// so the HTML <span> sibling stays in the HTML namespace.
|
|
142
|
+
expect(content).not.toContain('`<svg><svg')
|
|
143
|
+
expect(content).toContain('__tpl.content.firstElementChild.cloneNode(true)')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('static-loop CSR materialize with an SVG element root (#2219, #1247 path)', () => {
|
|
148
|
+
test('SVG-rooted materialize clone wraps and descends the extra level', () => {
|
|
149
|
+
const content = clientJsFor(`
|
|
150
|
+
'use client'
|
|
151
|
+
type Props = { ticks: Record<string, number> }
|
|
152
|
+
export function Repro(props: Props) {
|
|
153
|
+
const entries = Object.entries(props.ticks ?? {}).filter(([, y]) => y > 0)
|
|
154
|
+
return (
|
|
155
|
+
<svg viewBox="0 0 100 100">
|
|
156
|
+
{entries.map(([id, y]) => (
|
|
157
|
+
<line key={id} x1="0" x2="100" y1={y} y2={y} />
|
|
158
|
+
))}
|
|
159
|
+
</svg>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
`)
|
|
163
|
+
|
|
164
|
+
// Materialize branch present (unsafe prop-derived array, #1247) ...
|
|
165
|
+
expect(content).toMatch(/if \(!__iterEl\)/)
|
|
166
|
+
// ... and its template parse is namespace-aware.
|
|
167
|
+
expect(content).toMatch(/__tpl\.innerHTML = `<svg><line /)
|
|
168
|
+
expect(content).toContain('const __cloned = __tpl.content.firstElementChild.firstElementChild')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('multi-root (fragment) SVG materialize iterates the wrap one level down', () => {
|
|
172
|
+
const content = clientJsFor(`
|
|
173
|
+
'use client'
|
|
174
|
+
type Props = { ticks: Record<string, number> }
|
|
175
|
+
export function Repro(props: Props) {
|
|
176
|
+
const entries = Object.entries(props.ticks ?? {}).filter(([, y]) => y > 0)
|
|
177
|
+
return (
|
|
178
|
+
<svg viewBox="0 0 100 100">
|
|
179
|
+
{entries.map(([id, y]) => (
|
|
180
|
+
<>
|
|
181
|
+
<line key={id} x1="0" x2="100" y1={y} y2={y} />
|
|
182
|
+
<text x="0" y={y}>{id}</text>
|
|
183
|
+
</>
|
|
184
|
+
))}
|
|
185
|
+
</svg>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
`)
|
|
189
|
+
|
|
190
|
+
expect(content).toMatch(/__mtpl\.innerHTML = `<svg><line /)
|
|
191
|
+
// Sibling iteration starts inside the synthetic wrap.
|
|
192
|
+
expect(content).toContain('let __sib = __mtpl.content.firstElementChild.firstElementChild')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('<svg>-container-first fragment is NOT over-wrapped (#2233 review)', () => {
|
|
196
|
+
// An <svg> CONTAINER parses into the correct namespace bare — the HTML
|
|
197
|
+
// parser enters foreign content on its own. Wrapping the whole fragment
|
|
198
|
+
// would drag the HTML <span> sibling into the SVG namespace instead.
|
|
199
|
+
const content = clientJsFor(`
|
|
200
|
+
'use client'
|
|
201
|
+
type Props = { charts: Record<string, number> }
|
|
202
|
+
export function Repro(props: Props) {
|
|
203
|
+
const entries = Object.entries(props.charts ?? {}).filter(([, v]) => v > 0)
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
{entries.map(([id, v]) => (
|
|
207
|
+
<>
|
|
208
|
+
<svg key={id} viewBox="0 0 10 10"><circle r={v} /></svg>
|
|
209
|
+
<span>{id}</span>
|
|
210
|
+
</>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
`)
|
|
216
|
+
|
|
217
|
+
expect(content).toMatch(/__mtpl\.innerHTML = `<svg /)
|
|
218
|
+
expect(content).not.toContain('`<svg><svg')
|
|
219
|
+
// Sibling iteration starts at the template content directly (no wrap).
|
|
220
|
+
expect(content).toContain('let __sib = __mtpl.content.firstElementChild')
|
|
221
|
+
expect(content).not.toContain('let __sib = __mtpl.content.firstElementChild.firstElementChild')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('HTML-rooted materialize clone keeps the bare parse byte-identical', () => {
|
|
225
|
+
const content = clientJsFor(`
|
|
226
|
+
'use client'
|
|
227
|
+
type Props = { reactions: Record<string, string[]> }
|
|
228
|
+
export function Repro(props: Props) {
|
|
229
|
+
const entries = Object.entries(props.reactions ?? {}).filter(([, users]) => users.length > 0)
|
|
230
|
+
return (
|
|
231
|
+
<div>
|
|
232
|
+
{entries.map(([emoji, users]) => (
|
|
233
|
+
<button key={emoji} type="button">{emoji}: {String(users.length)}</button>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
`)
|
|
239
|
+
|
|
240
|
+
expect(content).toMatch(/if \(!__iterEl\)/)
|
|
241
|
+
expect(content).toMatch(/__tpl\.innerHTML = `<button /)
|
|
242
|
+
expect(content).toContain('const __cloned = __tpl.content.firstElementChild')
|
|
243
|
+
expect(content).not.toContain('`<svg>')
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { analyzeComponent } from '../analyzer'
|
|
3
|
+
import { jsxToIR } from '../jsx-to-ir'
|
|
4
|
+
import { collectLoopBoundNames } from '../adapters/loop-bound-names'
|
|
5
|
+
import type { ComponentIR } from '../types'
|
|
6
|
+
|
|
7
|
+
// #2212 (Fable review): `collectLoopBoundNames` finds every name a loop
|
|
8
|
+
// callback binds as its item/index parameter, so each adapter's
|
|
9
|
+
// `collectStringValueNames` can exclude it from string-concat detection —
|
|
10
|
+
// a loop param shadowing an outer string-typed binding of the same name
|
|
11
|
+
// must never be misdetected as string-typed.
|
|
12
|
+
describe('collectLoopBoundNames (#2212)', () => {
|
|
13
|
+
// `collectLoopBoundNames` only reads `.root`; the rest of `ComponentIR`
|
|
14
|
+
// is irrelevant to this walk, so a minimal stand-in is fine here.
|
|
15
|
+
function ir(source: string): ComponentIR {
|
|
16
|
+
const ctx = analyzeComponent(source, 'Test.tsx')
|
|
17
|
+
const root = jsxToIR(ctx)
|
|
18
|
+
if (!root) throw new Error('expected IR')
|
|
19
|
+
return { version: '0.1', metadata: {} as ComponentIR['metadata'], root, errors: [] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('a component with no loops has no bound names', () => {
|
|
23
|
+
const component = ir(`
|
|
24
|
+
function Test() { return <div>hi</div> }
|
|
25
|
+
export { Test }
|
|
26
|
+
`)
|
|
27
|
+
expect(collectLoopBoundNames(component).size).toBe(0)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('a simple loop param is bound', () => {
|
|
31
|
+
const component = ir(`
|
|
32
|
+
function Test({ items }: { items: string[] }) {
|
|
33
|
+
return <ul>{items.map(name => <li key={name}>{name}</li>)}</ul>
|
|
34
|
+
}
|
|
35
|
+
export { Test }
|
|
36
|
+
`)
|
|
37
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['name']))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('both item and index params are bound', () => {
|
|
41
|
+
const component = ir(`
|
|
42
|
+
function Test({ items }: { items: string[] }) {
|
|
43
|
+
return <ul>{items.map((name, idx) => <li key={idx}>{name}</li>)}</ul>
|
|
44
|
+
}
|
|
45
|
+
export { Test }
|
|
46
|
+
`)
|
|
47
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['name', 'idx']))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('nested loops contribute all their params', () => {
|
|
51
|
+
const component = ir(`
|
|
52
|
+
function Test({ groups }: { groups: { label: string; items: string[] }[] }) {
|
|
53
|
+
return <div>{groups.map(group => <ul key={group.label}>{group.items.map(item => <li key={item}>{item}</li>)}</ul>)}</div>
|
|
54
|
+
}
|
|
55
|
+
export { Test }
|
|
56
|
+
`)
|
|
57
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['group', 'item']))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('a loop body wrapping a child component still contributes its param', () => {
|
|
61
|
+
const component = ir(`
|
|
62
|
+
import { Row } from './row'
|
|
63
|
+
function Test({ items }: { items: string[] }) {
|
|
64
|
+
return <ul>{items.map(item => <Row key={item} label={item} />)}</ul>
|
|
65
|
+
}
|
|
66
|
+
export { Test }
|
|
67
|
+
`)
|
|
68
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['item']))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Fable re-review (#2212): `param` holds the raw pattern text for a
|
|
72
|
+
// destructured callback (`"{ name }"`), not the bound name itself — the
|
|
73
|
+
// real bound names live in `paramBindings`. Adapters that lower a
|
|
74
|
+
// destructure to a `{% set name = __bf_item.name %}`-style local (#2087)
|
|
75
|
+
// leave `name` reachable as a bare identifier in the loop body, so it
|
|
76
|
+
// needs the same exclusion as a plain loop param. `node.param`'s raw
|
|
77
|
+
// pattern text still ends up in the set too (added unconditionally,
|
|
78
|
+
// before this arm existed) — harmless noise, since it can never collide
|
|
79
|
+
// with a real identifier name.
|
|
80
|
+
test('a destructured loop param contributes its extracted binding names', () => {
|
|
81
|
+
const component = ir(`
|
|
82
|
+
function Test({ rows }: { rows: { name: string }[] }) {
|
|
83
|
+
return <ul>{rows.map(({ name }) => <li key={name}>{name}</li>)}</ul>
|
|
84
|
+
}
|
|
85
|
+
export { Test }
|
|
86
|
+
`)
|
|
87
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['{ name }', 'name']))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Fable re-review (#2212): a `.filter(pred).map(cb)` chain's filter
|
|
91
|
+
// predicate is emitted through the same binary/string-name machinery
|
|
92
|
+
// using its OWN param, which may differ from the map callback's `param`.
|
|
93
|
+
test('a filter().map() chain contributes both the filter predicate param and the map param', () => {
|
|
94
|
+
const component = ir(`
|
|
95
|
+
function Test({ values }: { values: number[] }) {
|
|
96
|
+
return <ul>{values.filter(n => n > 3).map(v => <li key={v}>{v}</li>)}</ul>
|
|
97
|
+
}
|
|
98
|
+
export { Test }
|
|
99
|
+
`)
|
|
100
|
+
expect(collectLoopBoundNames(component)).toEqual(new Set(['n', 'v']))
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -92,6 +92,7 @@ describe('buildLoopPlan decision tree (#1253)', () => {
|
|
|
92
92
|
kind: 'nested',
|
|
93
93
|
array: 'item.subs',
|
|
94
94
|
param: 'sub',
|
|
95
|
+
index: null,
|
|
95
96
|
key: null,
|
|
96
97
|
markerId: 'inner-m1',
|
|
97
98
|
template: '<span></span>',
|
|
@@ -161,6 +162,7 @@ describe('buildLoopPlan decision tree (#1253)', () => {
|
|
|
161
162
|
kind: 'nested',
|
|
162
163
|
array: 'item.subs',
|
|
163
164
|
param: 'sub',
|
|
165
|
+
index: null,
|
|
164
166
|
key: null,
|
|
165
167
|
markerId: 'inner-m1',
|
|
166
168
|
template: '<span></span>',
|