@pyreon/compiler 0.11.5 → 0.11.6
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/README.md +13 -10
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +4 -4
- package/package.json +10 -10
- package/src/index.ts +6 -11
- package/src/jsx.ts +104 -104
- package/src/project-scanner.ts +21 -21
- package/src/react-intercept.ts +213 -213
- package/src/tests/jsx.test.ts +583 -583
- package/src/tests/project-scanner.test.ts +63 -63
- package/src/tests/react-intercept.test.ts +280 -280
package/src/tests/jsx.test.ts
CHANGED
|
@@ -1,1110 +1,1110 @@
|
|
|
1
|
-
import { transformJSX } from
|
|
1
|
+
import { transformJSX } from '../jsx'
|
|
2
2
|
|
|
3
3
|
// Helper: transform and return the code string
|
|
4
|
-
const t = (code: string) => transformJSX(code,
|
|
4
|
+
const t = (code: string) => transformJSX(code, 'input.tsx').code
|
|
5
5
|
|
|
6
6
|
// ─── Children ────────────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
|
-
describe(
|
|
9
|
-
test(
|
|
10
|
-
const result = t(
|
|
11
|
-
expect(result).toContain(
|
|
8
|
+
describe('JSX transform — children', () => {
|
|
9
|
+
test('wraps dynamic child expression', () => {
|
|
10
|
+
const result = t('<div>{count()}</div>')
|
|
11
|
+
expect(result).toContain('_tpl(')
|
|
12
12
|
// Single-signal text binding uses _bindText for direct subscription
|
|
13
|
-
expect(result).toContain(
|
|
13
|
+
expect(result).toContain('_bindText(count,')
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
test(
|
|
17
|
-
expect(t(`<div>{"static"}</div>`)).not.toContain(
|
|
16
|
+
test('does NOT wrap string literal child', () => {
|
|
17
|
+
expect(t(`<div>{"static"}</div>`)).not.toContain('() =>')
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
test(
|
|
21
|
-
expect(t(
|
|
20
|
+
test('does NOT wrap numeric literal child', () => {
|
|
21
|
+
expect(t('<div>{42}</div>')).not.toContain('() =>')
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
test(
|
|
25
|
-
expect(t(
|
|
24
|
+
test('does NOT wrap null child', () => {
|
|
25
|
+
expect(t('<div>{null}</div>')).not.toContain('() =>')
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
test(
|
|
29
|
-
const result = t(
|
|
28
|
+
test('does NOT double-wrap existing arrow function', () => {
|
|
29
|
+
const result = t('<div>{() => count()}</div>')
|
|
30
30
|
// Arrow should be unwrapped by template emission into _bindText(count, __t)
|
|
31
31
|
// The original () => count() should NOT appear in the output
|
|
32
|
-
expect(result).toContain(
|
|
33
|
-
expect(result).not.toContain(
|
|
32
|
+
expect(result).toContain('_bindText(count,')
|
|
33
|
+
expect(result).not.toContain('() => count()')
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
test(
|
|
37
|
-
const result = t(
|
|
36
|
+
test('does NOT wrap a function expression child', () => {
|
|
37
|
+
const result = t('<div>{function() { return x }}</div>')
|
|
38
38
|
// Function expression body should be unwrapped by template emission
|
|
39
|
-
expect(result).toContain(
|
|
39
|
+
expect(result).toContain('_bind')
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
test(
|
|
43
|
-
expect(t(
|
|
42
|
+
test('does NOT wrap plain identifier (no call = not reactive)', () => {
|
|
43
|
+
expect(t('<div>{title}</div>')).not.toContain('() =>')
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
test(
|
|
47
|
-
expect(t(
|
|
46
|
+
test('does NOT wrap ternary without calls', () => {
|
|
47
|
+
expect(t('<div>{a ? b : c}</div>')).not.toContain('() =>')
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
test(
|
|
51
|
-
const result = t(
|
|
52
|
-
expect(result).toContain(
|
|
53
|
-
expect(result).toContain(
|
|
50
|
+
test('wraps ternary that contains a call', () => {
|
|
51
|
+
const result = t('<div>{a() ? b : c}</div>')
|
|
52
|
+
expect(result).toContain('_tpl(')
|
|
53
|
+
expect(result).toContain('.data = a() ? b : c')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
test(
|
|
57
|
-
expect(t(
|
|
56
|
+
test('does NOT wrap logical expression without calls', () => {
|
|
57
|
+
expect(t('<div>{show && <span />}</div>')).not.toContain('() =>')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
test(
|
|
61
|
-
expect(t(
|
|
60
|
+
test('wraps logical expression containing a call', () => {
|
|
61
|
+
expect(t('<div>{show() && <span />}</div>')).toContain('() => show() && <span />')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
test(
|
|
65
|
-
expect(t("<div>{{ color: 'red' }}</div>")).not.toContain(
|
|
64
|
+
test('does NOT wrap object literal child', () => {
|
|
65
|
+
expect(t("<div>{{ color: 'red' }}</div>")).not.toContain('() =>')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
test(
|
|
69
|
-
expect(t(
|
|
68
|
+
test('does NOT wrap array literal child', () => {
|
|
69
|
+
expect(t('<div>{[1, 2, 3]}</div>')).not.toContain('() =>')
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
test(
|
|
73
|
-
expect(t(
|
|
72
|
+
test('does NOT wrap boolean true literal', () => {
|
|
73
|
+
expect(t('<div>{true}</div>')).not.toContain('() =>')
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
test(
|
|
77
|
-
expect(t(
|
|
76
|
+
test('does NOT wrap boolean false literal', () => {
|
|
77
|
+
expect(t('<div>{false}</div>')).not.toContain('() =>')
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
test(
|
|
81
|
-
expect(t(
|
|
80
|
+
test('does NOT wrap undefined literal', () => {
|
|
81
|
+
expect(t('<div>{undefined}</div>')).not.toContain('() =>')
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
test(
|
|
85
|
-
expect(t(
|
|
84
|
+
test('does NOT wrap template literal without calls (no substitution)', () => {
|
|
85
|
+
expect(t('<div>{`hello`}</div>')).not.toContain('() =>')
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
test(
|
|
89
|
-
expect(t(
|
|
88
|
+
test('wraps template literal containing a call', () => {
|
|
89
|
+
expect(t('<div>{`hello ${name()}`}</div>')).toContain('() =>')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
test(
|
|
93
|
-
const result = t(
|
|
94
|
-
expect(result).toContain(
|
|
92
|
+
test('wraps member access with call', () => {
|
|
93
|
+
const result = t('<div>{obj.getValue()}</div>')
|
|
94
|
+
expect(result).toContain('_tpl(')
|
|
95
95
|
// Property access calls use _bind (not _bindText) to preserve this context
|
|
96
|
-
expect(result).toContain(
|
|
96
|
+
expect(result).toContain('_bind')
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
-
test(
|
|
100
|
-
expect(t(
|
|
99
|
+
test('does NOT wrap member access without call', () => {
|
|
100
|
+
expect(t('<div>{obj.value}</div>')).not.toContain('() =>')
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
test(
|
|
104
|
-
const result = t(
|
|
105
|
-
expect(result).toContain(
|
|
106
|
-
expect(result).toContain(
|
|
103
|
+
test('wraps binary expression containing a call', () => {
|
|
104
|
+
const result = t('<div>{count() + 1}</div>')
|
|
105
|
+
expect(result).toContain('_tpl(')
|
|
106
|
+
expect(result).toContain('.data = count() + 1')
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
test(
|
|
110
|
-
expect(t(
|
|
109
|
+
test('does NOT wrap binary expression without calls', () => {
|
|
110
|
+
expect(t('<div>{a + b}</div>')).not.toContain('() =>')
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
test(
|
|
114
|
-
expect(t(
|
|
113
|
+
test('wraps tagged template expression', () => {
|
|
114
|
+
expect(t('<div>{css`color: red`}</div>')).toContain('() =>')
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
test(
|
|
118
|
-
const result = t(
|
|
119
|
-
expect(result).toContain(
|
|
120
|
-
expect(result).toContain(
|
|
117
|
+
test('empty JSX expression {} gets _tpl optimization', () => {
|
|
118
|
+
const result = t('<div>{/* comment */}</div>')
|
|
119
|
+
expect(result).toContain('_tpl(')
|
|
120
|
+
expect(result).toContain('() => null')
|
|
121
121
|
})
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
// ─── Props ────────────────────────────────────────────────────────────────────
|
|
125
125
|
|
|
126
|
-
describe(
|
|
127
|
-
test(
|
|
128
|
-
expect(t(
|
|
126
|
+
describe('JSX transform — props', () => {
|
|
127
|
+
test('wraps dynamic class prop', () => {
|
|
128
|
+
expect(t('<div class={activeClass()} />')).toContain('() => activeClass()')
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
test(
|
|
132
|
-
expect(t(
|
|
131
|
+
test('wraps dynamic style prop', () => {
|
|
132
|
+
expect(t('<div style={styles()} />')).toContain('() => styles()')
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
test(
|
|
136
|
-
expect(t(`<div class="foo" />`)).not.toContain(
|
|
135
|
+
test('does NOT wrap string literal prop', () => {
|
|
136
|
+
expect(t(`<div class="foo" />`)).not.toContain('() =>')
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
test(
|
|
140
|
-
expect(t(`<div class={"foo"} />`)).not.toContain(
|
|
139
|
+
test('does NOT wrap JSX string attribute', () => {
|
|
140
|
+
expect(t(`<div class={"foo"} />`)).not.toContain('() =>')
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
test(
|
|
144
|
-
const result = t(
|
|
145
|
-
expect(result).not.toContain(
|
|
146
|
-
expect(result).toContain(
|
|
143
|
+
test('does NOT wrap onClick (event handler)', () => {
|
|
144
|
+
const result = t('<button onClick={handleClick} />')
|
|
145
|
+
expect(result).not.toContain('() => handleClick')
|
|
146
|
+
expect(result).toContain('handleClick') // still present
|
|
147
147
|
})
|
|
148
148
|
|
|
149
|
-
test(
|
|
150
|
-
expect(t(
|
|
149
|
+
test('does NOT wrap onInput (event handler)', () => {
|
|
150
|
+
expect(t('<input onInput={handler} />')).not.toContain('() => handler')
|
|
151
151
|
})
|
|
152
152
|
|
|
153
|
-
test(
|
|
154
|
-
expect(t(
|
|
153
|
+
test('does NOT wrap onMouseEnter (event handler)', () => {
|
|
154
|
+
expect(t('<div onMouseEnter={fn} />')).not.toContain('() => fn')
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
-
test(
|
|
158
|
-
expect(t(
|
|
157
|
+
test('does NOT wrap key prop', () => {
|
|
158
|
+
expect(t('<div key={id} />')).not.toContain('() => id')
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
test(
|
|
162
|
-
expect(t(
|
|
161
|
+
test('does NOT wrap ref prop', () => {
|
|
162
|
+
expect(t('<div ref={myRef} />')).not.toContain('() => myRef')
|
|
163
163
|
})
|
|
164
164
|
|
|
165
|
-
test(
|
|
166
|
-
const result = t(
|
|
165
|
+
test('does NOT wrap already-wrapped prop', () => {
|
|
166
|
+
const result = t('<div class={() => cls()} />')
|
|
167
167
|
expect(result.match(/\(\) =>/g)?.length).toBe(1)
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
-
test(
|
|
171
|
-
expect(t('<div style={{ color: "red" }} />')).not.toContain(
|
|
170
|
+
test('does NOT wrap object literal prop (style)', () => {
|
|
171
|
+
expect(t('<div style={{ color: "red" }} />')).not.toContain('() =>')
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
-
test(
|
|
175
|
-
expect(t(
|
|
174
|
+
test('wraps object literal prop when it contains a call', () => {
|
|
175
|
+
expect(t('<div style={{ color: theme() }} />')).toContain('() =>')
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
test(
|
|
178
|
+
test('does NOT wrap boolean shorthand attribute', () => {
|
|
179
179
|
// <input disabled /> — no initializer at all
|
|
180
|
-
expect(t(
|
|
180
|
+
expect(t('<input disabled />')).not.toContain('() =>')
|
|
181
181
|
})
|
|
182
182
|
|
|
183
|
-
test(
|
|
184
|
-
expect(t(
|
|
183
|
+
test('wraps dynamic data-* attribute', () => {
|
|
184
|
+
expect(t('<div data-id={getId()} />')).toContain('() => getId()')
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
test(
|
|
188
|
-
expect(t(
|
|
187
|
+
test('wraps dynamic aria-* attribute', () => {
|
|
188
|
+
expect(t('<div aria-label={getLabel()} />')).toContain('() => getLabel()')
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
test(
|
|
192
|
-
expect(t(
|
|
191
|
+
test('does NOT wrap onFocus (event handler)', () => {
|
|
192
|
+
expect(t('<input onFocus={handler} />')).not.toContain('() => handler')
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
test(
|
|
196
|
-
expect(t(
|
|
195
|
+
test('does NOT wrap onChange (event handler)', () => {
|
|
196
|
+
expect(t('<input onChange={handler} />')).not.toContain('() => handler')
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
test(
|
|
200
|
-
expect(t("<div title={isActive() ? 'yes' : 'no'} />")).toContain(
|
|
199
|
+
test('wraps conditional prop expression with call', () => {
|
|
200
|
+
expect(t("<div title={isActive() ? 'yes' : 'no'} />")).toContain('() =>')
|
|
201
201
|
})
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
// ─── Component elements ──────────────────────────────────────────────────────
|
|
205
205
|
|
|
206
|
-
describe(
|
|
207
|
-
test(
|
|
208
|
-
const result = t(
|
|
209
|
-
expect(result).not.toContain(
|
|
210
|
-
expect(result).toContain(
|
|
206
|
+
describe('JSX transform — component elements', () => {
|
|
207
|
+
test('does NOT wrap props on component elements (uppercase tag)', () => {
|
|
208
|
+
const result = t('<MyComponent value={count()} />')
|
|
209
|
+
expect(result).not.toContain('() => count()')
|
|
210
|
+
expect(result).toContain('count()')
|
|
211
211
|
})
|
|
212
212
|
|
|
213
|
-
test(
|
|
214
|
-
const result = t(
|
|
215
|
-
expect(result).not.toContain(
|
|
213
|
+
test('does NOT wrap any prop on uppercase component', () => {
|
|
214
|
+
const result = t('<Button label={getText()} />')
|
|
215
|
+
expect(result).not.toContain('() => getText()')
|
|
216
216
|
})
|
|
217
217
|
|
|
218
|
-
test(
|
|
218
|
+
test('wraps children of component elements (via JSX expression)', () => {
|
|
219
219
|
// Children in expression containers are still wrapped
|
|
220
|
-
const result = t(
|
|
221
|
-
expect(result).toContain(
|
|
220
|
+
const result = t('<MyComponent>{count()}</MyComponent>')
|
|
221
|
+
expect(result).toContain('() => count()')
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
test(
|
|
225
|
-
expect(t(
|
|
224
|
+
test('wraps props on lowercase DOM elements', () => {
|
|
225
|
+
expect(t('<div title={getTitle()} />')).toContain('() => getTitle()')
|
|
226
226
|
})
|
|
227
227
|
})
|
|
228
228
|
|
|
229
229
|
// ─── Spread attributes ──────────────────────────────────────────────────────
|
|
230
230
|
|
|
231
|
-
describe(
|
|
232
|
-
test(
|
|
233
|
-
const result = t(
|
|
231
|
+
describe('JSX transform — spread attributes', () => {
|
|
232
|
+
test('spread props are left unchanged (not wrapped)', () => {
|
|
233
|
+
const result = t('<div {...props} />')
|
|
234
234
|
// Spread should remain as-is, no reactive wrapping
|
|
235
|
-
expect(result).toContain(
|
|
236
|
-
expect(result).not.toContain(
|
|
235
|
+
expect(result).toContain('{...props}')
|
|
236
|
+
expect(result).not.toContain('() => ...props')
|
|
237
237
|
})
|
|
238
238
|
|
|
239
|
-
test(
|
|
240
|
-
const result = t(
|
|
241
|
-
expect(result).toContain(
|
|
242
|
-
expect(result).toContain(
|
|
239
|
+
test('spread with other props — only non-spread dynamic props get wrapped', () => {
|
|
240
|
+
const result = t('<div {...props} class={cls()} />')
|
|
241
|
+
expect(result).toContain('{...props}')
|
|
242
|
+
expect(result).toContain('() => cls()')
|
|
243
243
|
})
|
|
244
244
|
})
|
|
245
245
|
|
|
246
246
|
// ─── Static hoisting ─────────────────────────────────────────────────────────
|
|
247
247
|
|
|
248
|
-
describe(
|
|
249
|
-
test(
|
|
250
|
-
const result = t(
|
|
251
|
-
expect(result).toContain(
|
|
252
|
-
expect(result).toContain(
|
|
253
|
-
expect(result).toContain(
|
|
248
|
+
describe('JSX transform — static hoisting', () => {
|
|
249
|
+
test('hoists static JSX child to module scope', () => {
|
|
250
|
+
const result = t('<div>{<span>Hello</span>}</div>')
|
|
251
|
+
expect(result).toContain('const _$h0')
|
|
252
|
+
expect(result).toContain('<span>Hello</span>')
|
|
253
|
+
expect(result).toContain('{_$h0}')
|
|
254
254
|
})
|
|
255
255
|
|
|
256
|
-
test(
|
|
257
|
-
const result = t(
|
|
258
|
-
expect(result).toContain(
|
|
259
|
-
expect(result).toContain(
|
|
256
|
+
test('hoists static self-closing JSX', () => {
|
|
257
|
+
const result = t('<div>{<br />}</div>')
|
|
258
|
+
expect(result).toContain('const _$h0')
|
|
259
|
+
expect(result).toContain('{_$h0}')
|
|
260
260
|
})
|
|
261
261
|
|
|
262
|
-
test(
|
|
263
|
-
const result = t(
|
|
264
|
-
expect(result).not.toContain(
|
|
262
|
+
test('does NOT hoist JSX with dynamic props', () => {
|
|
263
|
+
const result = t('<div>{<span class={cls()}>text</span>}</div>')
|
|
264
|
+
expect(result).not.toContain('const _$h0')
|
|
265
265
|
})
|
|
266
266
|
|
|
267
|
-
test(
|
|
267
|
+
test('hoists JSX with static string prop', () => {
|
|
268
268
|
const result = t(`<div>{<span class="foo">text</span>}</div>`)
|
|
269
|
-
expect(result).toContain(
|
|
269
|
+
expect(result).toContain('const _$h0')
|
|
270
270
|
})
|
|
271
271
|
|
|
272
|
-
test(
|
|
273
|
-
const result = t(
|
|
274
|
-
expect(result).toContain(
|
|
275
|
-
expect(result).toContain(
|
|
272
|
+
test('hoists multiple static JSX children independently', () => {
|
|
273
|
+
const result = t('<div>{<span>A</span>}{<span>B</span>}</div>')
|
|
274
|
+
expect(result).toContain('const _$h0')
|
|
275
|
+
expect(result).toContain('const _$h1')
|
|
276
276
|
})
|
|
277
277
|
|
|
278
|
-
test(
|
|
279
|
-
const result = t(
|
|
280
|
-
expect(result).toContain(
|
|
278
|
+
test('hoists static fragment', () => {
|
|
279
|
+
const result = t('<div>{<>text</>}</div>')
|
|
280
|
+
expect(result).toContain('const _$h0')
|
|
281
281
|
})
|
|
282
282
|
|
|
283
|
-
test(
|
|
284
|
-
const result = t(
|
|
285
|
-
expect(result).not.toContain(
|
|
283
|
+
test('does NOT hoist fragment with dynamic child', () => {
|
|
284
|
+
const result = t('<div>{<>{count()}</>}</div>')
|
|
285
|
+
expect(result).not.toContain('const _$h0')
|
|
286
286
|
})
|
|
287
287
|
|
|
288
|
-
test(
|
|
289
|
-
const result = t(
|
|
290
|
-
expect(result).toContain(
|
|
288
|
+
test('hoisted declarations include @__PURE__ annotation', () => {
|
|
289
|
+
const result = t('<div>{<span>Hello</span>}</div>')
|
|
290
|
+
expect(result).toContain('/*@__PURE__*/')
|
|
291
291
|
})
|
|
292
292
|
|
|
293
|
-
test(
|
|
294
|
-
const result = t(
|
|
295
|
-
expect(result).not.toContain(
|
|
293
|
+
test('does NOT hoist JSX with spread attributes (always dynamic)', () => {
|
|
294
|
+
const result = t('<div>{<span {...props}>text</span>}</div>')
|
|
295
|
+
expect(result).not.toContain('const _$h0')
|
|
296
296
|
})
|
|
297
297
|
})
|
|
298
298
|
|
|
299
299
|
// ─── Mixed ────────────────────────────────────────────────────────────────────
|
|
300
300
|
|
|
301
|
-
describe(
|
|
302
|
-
test(
|
|
303
|
-
const result = t(
|
|
304
|
-
expect(result).toContain(
|
|
301
|
+
describe('JSX transform — mixed', () => {
|
|
302
|
+
test('wraps props and children independently', () => {
|
|
303
|
+
const result = t('<div class={cls()}>{text()}</div>')
|
|
304
|
+
expect(result).toContain('_tpl(')
|
|
305
305
|
// className uses _bindDirect (single-signal), text uses _bindText
|
|
306
|
-
expect(result).toContain(
|
|
307
|
-
expect(result).toContain(
|
|
306
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
307
|
+
expect(result).toContain('_bindText(text,')
|
|
308
308
|
})
|
|
309
309
|
|
|
310
|
-
test(
|
|
311
|
-
const result = t(
|
|
312
|
-
expect(result).toContain(
|
|
313
|
-
expect(result).toContain(
|
|
314
|
-
expect(result).toContain(
|
|
310
|
+
test('preserves static siblings of dynamic children', () => {
|
|
311
|
+
const result = t('<div>static{count()}</div>')
|
|
312
|
+
expect(result).toContain('_tpl(')
|
|
313
|
+
expect(result).toContain('static')
|
|
314
|
+
expect(result).toContain('_bindText(count,')
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
test(
|
|
318
|
-
const input =
|
|
317
|
+
test('leaves code outside JSX completely unchanged', () => {
|
|
318
|
+
const input = 'const x = count() + 1'
|
|
319
319
|
expect(t(input)).toBe(input)
|
|
320
320
|
})
|
|
321
321
|
|
|
322
|
-
test(
|
|
322
|
+
test('handles multiple JSX elements in one file', () => {
|
|
323
323
|
const input = `
|
|
324
324
|
const A = <div>{a()}</div>
|
|
325
325
|
const B = <span>{b()}</span>
|
|
326
326
|
`
|
|
327
327
|
const result = t(input)
|
|
328
|
-
expect(result).toContain(
|
|
329
|
-
expect(result).toContain(
|
|
330
|
-
expect(result).toContain(
|
|
328
|
+
expect(result).toContain('_tpl(')
|
|
329
|
+
expect(result).toContain('_bindText(a,')
|
|
330
|
+
expect(result).toContain('_bindText(b,')
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
-
test(
|
|
334
|
-
const result = t(
|
|
333
|
+
test('handles deeply nested JSX', () => {
|
|
334
|
+
const result = t('<div><span><em>{count()}</em></span></div>')
|
|
335
335
|
// Template emission: 3 DOM elements → _tpl() call with _bindText binding
|
|
336
|
-
expect(result).toContain(
|
|
337
|
-
expect(result).toContain(
|
|
336
|
+
expect(result).toContain('_tpl(')
|
|
337
|
+
expect(result).toContain('_bindText(count,')
|
|
338
338
|
})
|
|
339
339
|
|
|
340
|
-
test(
|
|
341
|
-
const input =
|
|
340
|
+
test('returns unchanged code when no JSX present', () => {
|
|
341
|
+
const input = 'const x = 1 + 2'
|
|
342
342
|
expect(t(input)).toBe(input)
|
|
343
343
|
})
|
|
344
344
|
|
|
345
|
-
test(
|
|
346
|
-
const result = t(
|
|
347
|
-
expect(result).toContain(
|
|
348
|
-
expect(result).toContain(
|
|
349
|
-
expect(result).toContain(
|
|
345
|
+
test('handles empty JSX element', () => {
|
|
346
|
+
const result = t('<div></div>')
|
|
347
|
+
expect(result).toContain('_tpl(')
|
|
348
|
+
expect(result).toContain('<div></div>')
|
|
349
|
+
expect(result).toContain('() => null')
|
|
350
350
|
})
|
|
351
351
|
|
|
352
|
-
test(
|
|
353
|
-
const result = t(
|
|
354
|
-
expect(result).toBe(
|
|
352
|
+
test('handles self-closing element with no props', () => {
|
|
353
|
+
const result = t('<br />')
|
|
354
|
+
expect(result).toBe('<br />')
|
|
355
355
|
})
|
|
356
356
|
})
|
|
357
357
|
|
|
358
358
|
// ─── Edge cases ──────────────────────────────────────────────────────────────
|
|
359
359
|
|
|
360
|
-
describe(
|
|
361
|
-
test(
|
|
362
|
-
expect(t(
|
|
360
|
+
describe('JSX transform — edge cases', () => {
|
|
361
|
+
test('wraps chained method call', () => {
|
|
362
|
+
expect(t('<div>{items().map(x => x)}</div>')).toContain('() =>')
|
|
363
363
|
})
|
|
364
364
|
|
|
365
|
-
test(
|
|
365
|
+
test('does not emit _bindText for method calls (preserves this context)', () => {
|
|
366
366
|
// value.toLocaleString() — property access must NOT use _bindText
|
|
367
367
|
// because detaching the method loses `this` context
|
|
368
|
-
const result = t(
|
|
369
|
-
expect(result).not.toContain(
|
|
370
|
-
expect(result).toContain(
|
|
368
|
+
const result = t('<div><p>{value.toLocaleString()}</p></div>')
|
|
369
|
+
expect(result).not.toContain('_bindText(value.toLocaleString,')
|
|
370
|
+
expect(result).toContain('_bind')
|
|
371
371
|
})
|
|
372
372
|
|
|
373
|
-
test(
|
|
373
|
+
test('toLocaleString on signal read preserves this context', () => {
|
|
374
374
|
// {() => count().toLocaleString()} should NOT detach .toLocaleString
|
|
375
|
-
const result = t(
|
|
376
|
-
expect(result).not.toContain(
|
|
375
|
+
const result = t('<div>{() => count().toLocaleString()}</div>')
|
|
376
|
+
expect(result).not.toContain('_bindText(count,')
|
|
377
377
|
// The arrow wraps a chained call — it should use _bind, not _bindText
|
|
378
|
-
expect(result).toContain(
|
|
378
|
+
expect(result).toContain('_bind')
|
|
379
379
|
})
|
|
380
380
|
|
|
381
|
-
test(
|
|
382
|
-
expect(t(
|
|
381
|
+
test('wraps nested call in array expression', () => {
|
|
382
|
+
expect(t('<div>{[getItem()]}</div>')).toContain('() =>')
|
|
383
383
|
})
|
|
384
384
|
|
|
385
|
-
test(
|
|
386
|
-
const result = t(
|
|
387
|
-
expect(result).toContain(
|
|
388
|
-
expect(result).toContain(
|
|
389
|
-
expect(result).toContain(
|
|
385
|
+
test('handles JSX with only text children (no expression)', () => {
|
|
386
|
+
const result = t('<div>hello world</div>')
|
|
387
|
+
expect(result).toContain('_tpl(')
|
|
388
|
+
expect(result).toContain('hello world')
|
|
389
|
+
expect(result).toContain('() => null')
|
|
390
390
|
})
|
|
391
391
|
|
|
392
|
-
test(
|
|
393
|
-
const result = t(
|
|
394
|
-
expect(result).not.toContain(
|
|
392
|
+
test('does NOT wrap arrow function with params', () => {
|
|
393
|
+
const result = t('<div>{(x: number) => x + 1}</div>')
|
|
394
|
+
expect(result).not.toContain('() => (x')
|
|
395
395
|
})
|
|
396
396
|
|
|
397
|
-
test(
|
|
398
|
-
const result = transformJSX(
|
|
399
|
-
expect(result).toContain(
|
|
400
|
-
expect(result).toContain(
|
|
397
|
+
test('handles .jsx file extension', () => {
|
|
398
|
+
const result = transformJSX('<div>{count()}</div>', 'file.jsx').code
|
|
399
|
+
expect(result).toContain('_tpl(')
|
|
400
|
+
expect(result).toContain('_bindText(count,')
|
|
401
401
|
})
|
|
402
402
|
|
|
403
|
-
test(
|
|
404
|
-
const result = transformJSX(
|
|
405
|
-
expect(result).toContain(
|
|
406
|
-
expect(result).toContain(
|
|
403
|
+
test('handles .ts file extension (treated as TSX)', () => {
|
|
404
|
+
const result = transformJSX('<div>{count()}</div>', 'file.ts').code
|
|
405
|
+
expect(result).toContain('_tpl(')
|
|
406
|
+
expect(result).toContain('_bindText(count,')
|
|
407
407
|
})
|
|
408
408
|
|
|
409
|
-
test(
|
|
410
|
-
expect(t(
|
|
409
|
+
test('wraps call inside array map', () => {
|
|
410
|
+
expect(t('<ul>{items().map(i => <li>{i}</li>)}</ul>')).toContain('() =>')
|
|
411
411
|
})
|
|
412
412
|
|
|
413
|
-
test(
|
|
414
|
-
const result = t(
|
|
413
|
+
test('does NOT wrap callback function expression inside event prop', () => {
|
|
414
|
+
const result = t('<button onClick={() => doSomething()} />')
|
|
415
415
|
// onClick is an event handler, should not be wrapped at all
|
|
416
|
-
expect(result).not.toContain(
|
|
416
|
+
expect(result).not.toContain('() => () =>')
|
|
417
417
|
})
|
|
418
418
|
|
|
419
|
-
test(
|
|
420
|
-
expect(t(
|
|
419
|
+
test('wraps call deep in property access chain', () => {
|
|
420
|
+
expect(t('<div>{store.getState().count}</div>')).toContain('() =>')
|
|
421
421
|
})
|
|
422
422
|
|
|
423
|
-
test(
|
|
424
|
-
const result = t(
|
|
425
|
-
expect(result).not.toContain(
|
|
423
|
+
test('does NOT wrap function expression child (named)', () => {
|
|
424
|
+
const result = t('<div>{function foo() { return 1 }}</div>')
|
|
425
|
+
expect(result).not.toContain('() => function')
|
|
426
426
|
})
|
|
427
427
|
})
|
|
428
428
|
|
|
429
429
|
// ─── TransformResult type ────────────────────────────────────────────────────
|
|
430
430
|
|
|
431
|
-
describe(
|
|
432
|
-
test(
|
|
433
|
-
const result = transformJSX(
|
|
434
|
-
expect(typeof result.code).toBe(
|
|
431
|
+
describe('transformJSX return value', () => {
|
|
432
|
+
test('returns object with code property', () => {
|
|
433
|
+
const result = transformJSX('<div>{count()}</div>')
|
|
434
|
+
expect(typeof result.code).toBe('string')
|
|
435
435
|
})
|
|
436
436
|
|
|
437
|
-
test(
|
|
437
|
+
test('default filename is input.tsx', () => {
|
|
438
438
|
// Should not throw with default filename
|
|
439
|
-
const result = transformJSX(
|
|
440
|
-
expect(result.code).toContain(
|
|
441
|
-
expect(result.code).toContain(
|
|
439
|
+
const result = transformJSX('<div>{count()}</div>')
|
|
440
|
+
expect(result.code).toContain('_tpl(')
|
|
441
|
+
expect(result.code).toContain('_bindText(count,')
|
|
442
442
|
})
|
|
443
443
|
})
|
|
444
444
|
|
|
445
445
|
// ─── Template emission ──────────────────────────────────────────────────────
|
|
446
446
|
|
|
447
|
-
describe(
|
|
448
|
-
test(
|
|
449
|
-
const result = t(
|
|
450
|
-
expect(result).toContain(
|
|
451
|
-
expect(result).toContain(
|
|
447
|
+
describe('JSX transform — template emission', () => {
|
|
448
|
+
test('emits _tpl for 2+ element tree', () => {
|
|
449
|
+
const result = t('<div><span>hello</span></div>')
|
|
450
|
+
expect(result).toContain('_tpl(')
|
|
451
|
+
expect(result).toContain('<div><span>hello</span></div>')
|
|
452
452
|
})
|
|
453
453
|
|
|
454
|
-
test(
|
|
455
|
-
const result = t(
|
|
456
|
-
expect(result).toContain(
|
|
457
|
-
expect(result).toContain(
|
|
454
|
+
test('emits _tpl for single element', () => {
|
|
455
|
+
const result = t('<div>hello</div>')
|
|
456
|
+
expect(result).toContain('_tpl(')
|
|
457
|
+
expect(result).toContain('hello')
|
|
458
458
|
})
|
|
459
459
|
|
|
460
|
-
test(
|
|
461
|
-
const result = t(
|
|
462
|
-
expect(result).not.toContain(
|
|
460
|
+
test('does NOT emit _tpl for component elements', () => {
|
|
461
|
+
const result = t('<div><MyComponent /></div>')
|
|
462
|
+
expect(result).not.toContain('_tpl(')
|
|
463
463
|
})
|
|
464
464
|
|
|
465
|
-
test(
|
|
466
|
-
const result = t(
|
|
467
|
-
expect(result).not.toContain(
|
|
465
|
+
test('does NOT emit _tpl for spread attributes', () => {
|
|
466
|
+
const result = t('<div {...props}><span /></div>')
|
|
467
|
+
expect(result).not.toContain('_tpl(')
|
|
468
468
|
})
|
|
469
469
|
|
|
470
|
-
test(
|
|
471
|
-
const result = t(
|
|
472
|
-
expect(result).not.toContain(
|
|
470
|
+
test('does NOT emit _tpl for keyed elements', () => {
|
|
471
|
+
const result = t('<div key={id}><span /></div>')
|
|
472
|
+
expect(result).not.toContain('_tpl(')
|
|
473
473
|
})
|
|
474
474
|
|
|
475
|
-
test(
|
|
475
|
+
test('bakes static string attributes into HTML', () => {
|
|
476
476
|
const result = t('<div class="box"><span /></div>')
|
|
477
477
|
// Quotes are escaped inside the _tpl("...") string literal
|
|
478
478
|
expect(result).toContain('class=\\"box\\"')
|
|
479
|
-
expect(result).toContain(
|
|
479
|
+
expect(result).toContain('_tpl(')
|
|
480
480
|
})
|
|
481
481
|
|
|
482
|
-
test(
|
|
483
|
-
const result = t(
|
|
484
|
-
expect(result).toContain(
|
|
485
|
-
expect(result).toContain(
|
|
482
|
+
test('bakes boolean shorthand attributes into HTML', () => {
|
|
483
|
+
const result = t('<div><input disabled /></div>')
|
|
484
|
+
expect(result).toContain(' disabled')
|
|
485
|
+
expect(result).toContain('_tpl(')
|
|
486
486
|
})
|
|
487
487
|
|
|
488
|
-
test(
|
|
489
|
-
const result = t(
|
|
490
|
-
expect(result).toContain(
|
|
491
|
-
expect(result).toContain(
|
|
488
|
+
test('generates _bindDirect for reactive class with single signal', () => {
|
|
489
|
+
const result = t('<div class={cls()}><span /></div>')
|
|
490
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
491
|
+
expect(result).toContain('className')
|
|
492
492
|
})
|
|
493
493
|
|
|
494
|
-
test(
|
|
495
|
-
const result = t(
|
|
496
|
-
expect(result).toContain(
|
|
494
|
+
test('generates _bindText for reactive text child with single signal', () => {
|
|
495
|
+
const result = t('<div><span>{name()}</span></div>')
|
|
496
|
+
expect(result).toContain('_bindText(name,')
|
|
497
497
|
})
|
|
498
498
|
|
|
499
|
-
test(
|
|
500
|
-
const result = t(
|
|
501
|
-
expect(result).toContain(
|
|
502
|
-
expect(result).not.toContain(
|
|
499
|
+
test('generates one-time set for static expression text', () => {
|
|
500
|
+
const result = t('<div><span>{label}</span></div>')
|
|
501
|
+
expect(result).toContain('textContent = label')
|
|
502
|
+
expect(result).not.toContain('_bind(')
|
|
503
503
|
})
|
|
504
504
|
|
|
505
|
-
test(
|
|
506
|
-
const result = t(
|
|
505
|
+
test('generates delegated event for common events', () => {
|
|
506
|
+
const result = t('<div><button onClick={handler}>click</button></div>')
|
|
507
507
|
// click is delegated — uses expando property instead of addEventListener
|
|
508
|
-
expect(result).toContain(
|
|
508
|
+
expect(result).toContain('__ev_click = handler')
|
|
509
509
|
})
|
|
510
510
|
|
|
511
|
-
test(
|
|
512
|
-
const result = t(
|
|
511
|
+
test('uses element children indexing for nested access', () => {
|
|
512
|
+
const result = t('<div><span>{a()}</span><em>{b()}</em></div>')
|
|
513
513
|
// Can't have two expression children in same parent, but each is in its own element
|
|
514
|
-
expect(result).toContain(
|
|
515
|
-
expect(result).toContain(
|
|
514
|
+
expect(result).toContain('__root.children[0]')
|
|
515
|
+
expect(result).toContain('__root.children[1]')
|
|
516
516
|
})
|
|
517
517
|
|
|
518
|
-
test(
|
|
519
|
-
const result = t(
|
|
520
|
-
expect(result).toContain(
|
|
521
|
-
expect(result).toContain(
|
|
518
|
+
test('handles deeply nested element paths', () => {
|
|
519
|
+
const result = t('<table><tbody><tr><td>{text()}</td></tr></tbody></table>')
|
|
520
|
+
expect(result).toContain('_tpl(')
|
|
521
|
+
expect(result).toContain('_bindText(text,')
|
|
522
522
|
})
|
|
523
523
|
|
|
524
|
-
test(
|
|
525
|
-
const result = transformJSX(
|
|
524
|
+
test('adds template imports when _tpl is emitted', () => {
|
|
525
|
+
const result = transformJSX('<div><span>text</span></div>')
|
|
526
526
|
expect(result.code).toContain('import { _tpl } from "@pyreon/runtime-dom"')
|
|
527
527
|
expect(result.usesTemplates).toBe(true)
|
|
528
528
|
})
|
|
529
529
|
|
|
530
|
-
test(
|
|
531
|
-
const result = transformJSX(
|
|
530
|
+
test('adds template imports for single element', () => {
|
|
531
|
+
const result = transformJSX('<div>text</div>')
|
|
532
532
|
expect(result.code).toContain('import { _tpl } from "@pyreon/runtime-dom"')
|
|
533
533
|
expect(result.usesTemplates).toBe(true)
|
|
534
534
|
})
|
|
535
535
|
|
|
536
|
-
test(
|
|
536
|
+
test('wraps _tpl call in braces when child of JSX element', () => {
|
|
537
537
|
// <Comp> is a component, so outer element is not templateized
|
|
538
538
|
// but <span><em> inside it has 2 elements
|
|
539
|
-
const result = t(
|
|
539
|
+
const result = t('<Comp><span><em>text</em></span></Comp>')
|
|
540
540
|
// The inner span+em gets templateized inside the component children
|
|
541
|
-
expect(result).toContain(
|
|
541
|
+
expect(result).toContain('{_tpl(')
|
|
542
542
|
})
|
|
543
543
|
|
|
544
|
-
test(
|
|
545
|
-
const result = t(
|
|
546
|
-
expect(result).toContain(
|
|
547
|
-
expect(result).toContain(
|
|
548
|
-
expect(result).not.toContain(
|
|
544
|
+
test('handles self-closing void elements in template', () => {
|
|
545
|
+
const result = t('<div><br /><span>text</span></div>')
|
|
546
|
+
expect(result).toContain('_tpl(')
|
|
547
|
+
expect(result).toContain('<br>')
|
|
548
|
+
expect(result).not.toContain('</br>')
|
|
549
549
|
})
|
|
550
550
|
|
|
551
|
-
test(
|
|
551
|
+
test('handles mixed static text and element children', () => {
|
|
552
552
|
const result = t('<div class="c"><span>inner</span></div>')
|
|
553
|
-
expect(result).toContain(
|
|
554
|
-
expect(result).toContain(
|
|
553
|
+
expect(result).toContain('_tpl(')
|
|
554
|
+
expect(result).toContain('<span>inner</span>')
|
|
555
555
|
})
|
|
556
556
|
|
|
557
|
-
test(
|
|
557
|
+
test('escapes quotes in HTML attribute values', () => {
|
|
558
558
|
const result = t('<div title="say "hi""><span /></div>')
|
|
559
|
-
expect(result).toContain(
|
|
559
|
+
expect(result).toContain('_tpl(')
|
|
560
560
|
})
|
|
561
561
|
|
|
562
|
-
test(
|
|
563
|
-
const result = t(
|
|
564
|
-
expect(result).toContain(
|
|
562
|
+
test('returns null cleanup when no dynamic bindings', () => {
|
|
563
|
+
const result = t('<div><span>static</span></div>')
|
|
564
|
+
expect(result).toContain('() => null')
|
|
565
565
|
})
|
|
566
566
|
|
|
567
|
-
test(
|
|
568
|
-
const result = t(
|
|
569
|
-
expect(result).toContain(
|
|
570
|
-
expect(result).toContain(
|
|
567
|
+
test('composes multiple disposers in cleanup', () => {
|
|
568
|
+
const result = t('<div class={a()}><span>{b()}</span></div>')
|
|
569
|
+
expect(result).toContain('__d0()')
|
|
570
|
+
expect(result).toContain('__d1()')
|
|
571
571
|
})
|
|
572
572
|
|
|
573
|
-
test(
|
|
573
|
+
test('maps className to class in HTML', () => {
|
|
574
574
|
const result = t('<div className="box"><span /></div>')
|
|
575
575
|
// Quotes escaped in _tpl string literal
|
|
576
576
|
expect(result).toContain('class=\\"box\\"')
|
|
577
|
-
expect(result).not.toContain(
|
|
577
|
+
expect(result).not.toContain('className')
|
|
578
578
|
})
|
|
579
579
|
|
|
580
|
-
test(
|
|
580
|
+
test('maps htmlFor to for in HTML', () => {
|
|
581
581
|
const result = t('<div><label htmlFor="name">Name</label></div>')
|
|
582
582
|
expect(result).toContain('for=\\"name\\"')
|
|
583
583
|
})
|
|
584
584
|
|
|
585
|
-
test(
|
|
586
|
-
const result = t(
|
|
585
|
+
test('inlines fragments inside template', () => {
|
|
586
|
+
const result = t('<div><><span>text</span></></div>')
|
|
587
587
|
// Fragment children are inlined as direct children
|
|
588
|
-
expect(result).toContain(
|
|
589
|
-
expect(result).toContain(
|
|
588
|
+
expect(result).toContain('_tpl(')
|
|
589
|
+
expect(result).toContain('<span>text</span>')
|
|
590
590
|
})
|
|
591
591
|
|
|
592
|
-
test(
|
|
593
|
-
const result = t(
|
|
594
|
-
expect(result).not.toContain(
|
|
592
|
+
test('bails on expression children containing JSX', () => {
|
|
593
|
+
const result = t('<div><span />{show() && <em />}</div>')
|
|
594
|
+
expect(result).not.toContain('_tpl(')
|
|
595
595
|
})
|
|
596
596
|
|
|
597
|
-
test(
|
|
598
|
-
const result = t(
|
|
597
|
+
test('handles mixed element + expression children', () => {
|
|
598
|
+
const result = t('<div><span />{text()}</div>')
|
|
599
599
|
// Mixed element + expression children use childNodes indexing
|
|
600
|
-
expect(result).toContain(
|
|
601
|
-
expect(result).toContain(
|
|
602
|
-
expect(result).toContain(
|
|
600
|
+
expect(result).toContain('_tpl(')
|
|
601
|
+
expect(result).toContain('childNodes[')
|
|
602
|
+
expect(result).toContain('_bindText(text,')
|
|
603
603
|
})
|
|
604
604
|
|
|
605
|
-
test(
|
|
605
|
+
test('benchmark-like row structure', () => {
|
|
606
606
|
const result = t(
|
|
607
607
|
'<tr class={cls()}><td class="id">{String(row.id)}</td><td>{row.label()}</td></tr>',
|
|
608
608
|
)
|
|
609
|
-
expect(result).toContain(
|
|
609
|
+
expect(result).toContain('_tpl(')
|
|
610
610
|
expect(result).toContain('<td class=\\"id\\"></td><td></td>')
|
|
611
611
|
// className uses _bindDirect (single-signal cls())
|
|
612
|
-
expect(result).toContain(
|
|
612
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
613
613
|
// String(row.id) has args → combined _bind; row.label() is property access → _bind
|
|
614
|
-
expect(result).toContain(
|
|
615
|
-
expect(result).toContain(
|
|
614
|
+
expect(result).toContain('.data = String(row.id)')
|
|
615
|
+
expect(result).toContain('row.label()')
|
|
616
616
|
})
|
|
617
617
|
|
|
618
|
-
test(
|
|
619
|
-
const result = t(
|
|
620
|
-
expect(result).toContain(
|
|
618
|
+
test('handles multiple expression children', () => {
|
|
619
|
+
const result = t('<div><span>{a()}{b()}</span></div>')
|
|
620
|
+
expect(result).toContain('_tpl(')
|
|
621
621
|
// Both are single-signal → _bindText
|
|
622
|
-
expect(result).toContain(
|
|
623
|
-
expect(result).toContain(
|
|
622
|
+
expect(result).toContain('_bindText(a,')
|
|
623
|
+
expect(result).toContain('_bindText(b,')
|
|
624
624
|
// Each expression gets its own placeholder and childNodes access
|
|
625
|
-
expect(result).toContain(
|
|
626
|
-
expect(result).toContain(
|
|
625
|
+
expect(result).toContain('childNodes[0]')
|
|
626
|
+
expect(result).toContain('childNodes[1]')
|
|
627
627
|
})
|
|
628
628
|
|
|
629
|
-
test(
|
|
630
|
-
const result = t(
|
|
631
|
-
expect(result).toContain(
|
|
632
|
-
expect(result).toContain(
|
|
633
|
-
expect(result).toContain(
|
|
629
|
+
test('handles mixed text + element + expression children', () => {
|
|
630
|
+
const result = t('<div>hello<span />{name()}</div>')
|
|
631
|
+
expect(result).toContain('_tpl(')
|
|
632
|
+
expect(result).toContain('childNodes[')
|
|
633
|
+
expect(result).toContain('_bindText(name,')
|
|
634
634
|
})
|
|
635
635
|
|
|
636
|
-
test(
|
|
637
|
-
const result = t(
|
|
638
|
-
expect(result).toContain(
|
|
639
|
-
expect(result).toContain(
|
|
640
|
-
expect(result).toContain(
|
|
636
|
+
test('handles fragment with element children inside template', () => {
|
|
637
|
+
const result = t('<div><><span>a</span><em>b</em></></div>')
|
|
638
|
+
expect(result).toContain('_tpl(')
|
|
639
|
+
expect(result).toContain('<span>a</span>')
|
|
640
|
+
expect(result).toContain('<em>b</em>')
|
|
641
641
|
})
|
|
642
642
|
|
|
643
|
-
test(
|
|
644
|
-
const result = t(
|
|
645
|
-
expect(result).not.toContain(
|
|
643
|
+
test('bails on fragment with non-eligible children', () => {
|
|
644
|
+
const result = t('<div><><Component /></></div>')
|
|
645
|
+
expect(result).not.toContain('_tpl(')
|
|
646
646
|
})
|
|
647
647
|
|
|
648
|
-
test(
|
|
649
|
-
const result = t(
|
|
650
|
-
expect(result).toContain(
|
|
651
|
-
expect(result).toContain(
|
|
652
|
-
expect(result).toContain(
|
|
648
|
+
test('handles static expression in mixed children', () => {
|
|
649
|
+
const result = t('<div><span />{label}</div>')
|
|
650
|
+
expect(result).toContain('_tpl(')
|
|
651
|
+
expect(result).toContain('childNodes[')
|
|
652
|
+
expect(result).toContain('createTextNode(label)')
|
|
653
653
|
})
|
|
654
654
|
|
|
655
|
-
test(
|
|
656
|
-
const result = t(
|
|
657
|
-
expect(result).toContain(
|
|
655
|
+
test('bakes static numeric literal attr into HTML', () => {
|
|
656
|
+
const result = t('<div tabindex={0}><span /></div>')
|
|
657
|
+
expect(result).toContain('_tpl(')
|
|
658
658
|
expect(result).toContain('tabindex=\\"0\\"')
|
|
659
659
|
})
|
|
660
660
|
|
|
661
|
-
test(
|
|
662
|
-
const result = t(
|
|
663
|
-
expect(result).toContain(
|
|
664
|
-
expect(result).toContain(
|
|
661
|
+
test('bakes static true keyword attr into HTML', () => {
|
|
662
|
+
const result = t('<div hidden={true}><span /></div>')
|
|
663
|
+
expect(result).toContain('_tpl(')
|
|
664
|
+
expect(result).toContain(' hidden')
|
|
665
665
|
})
|
|
666
666
|
|
|
667
|
-
test(
|
|
668
|
-
const result = t(
|
|
669
|
-
expect(result).toContain(
|
|
670
|
-
expect(result).not.toContain(
|
|
667
|
+
test('omits false keyword attr from HTML', () => {
|
|
668
|
+
const result = t('<div hidden={false}><span /></div>')
|
|
669
|
+
expect(result).toContain('_tpl(')
|
|
670
|
+
expect(result).not.toContain('hidden')
|
|
671
671
|
})
|
|
672
672
|
|
|
673
|
-
test(
|
|
674
|
-
const result = t(
|
|
675
|
-
expect(result).toContain(
|
|
676
|
-
expect(result).not.toContain(
|
|
673
|
+
test('omits null keyword attr from HTML', () => {
|
|
674
|
+
const result = t('<div hidden={null}><span /></div>')
|
|
675
|
+
expect(result).toContain('_tpl(')
|
|
676
|
+
expect(result).not.toContain('hidden')
|
|
677
677
|
})
|
|
678
678
|
|
|
679
|
-
test(
|
|
680
|
-
const result = t(
|
|
681
|
-
expect(result).toContain(
|
|
679
|
+
test('emits setAttribute for undefined keyword attr', () => {
|
|
680
|
+
const result = t('<div hidden={undefined}><span /></div>')
|
|
681
|
+
expect(result).toContain('_tpl(')
|
|
682
682
|
expect(result).toContain('setAttribute("hidden", undefined)')
|
|
683
683
|
})
|
|
684
684
|
|
|
685
|
-
test(
|
|
686
|
-
const result = t(
|
|
685
|
+
test('one-time set for non-class static expression attribute', () => {
|
|
686
|
+
const result = t('<div title={someVar}><span /></div>')
|
|
687
687
|
expect(result).toContain('setAttribute("title", someVar)')
|
|
688
|
-
expect(result).not.toContain(
|
|
688
|
+
expect(result).not.toContain('_bind(')
|
|
689
689
|
})
|
|
690
690
|
|
|
691
|
-
test(
|
|
692
|
-
const result = t(
|
|
693
|
-
expect(result).toContain(
|
|
691
|
+
test('_bindDirect for non-class single-signal dynamic attribute', () => {
|
|
692
|
+
const result = t('<div title={getTitle()}><span /></div>')
|
|
693
|
+
expect(result).toContain('_bindDirect(getTitle,')
|
|
694
694
|
expect(result).toContain('setAttribute("title"')
|
|
695
695
|
})
|
|
696
696
|
|
|
697
|
-
test(
|
|
698
|
-
const result = t(
|
|
699
|
-
expect(result).toContain(
|
|
697
|
+
test('ref attribute in template binds .current', () => {
|
|
698
|
+
const result = t('<div ref={myRef}><span /></div>')
|
|
699
|
+
expect(result).toContain('myRef.current = __root')
|
|
700
700
|
})
|
|
701
701
|
|
|
702
|
-
test(
|
|
703
|
-
const result = t(
|
|
704
|
-
expect(result).toContain(
|
|
705
|
-
expect(result).toContain(
|
|
702
|
+
test('handles non-void self-closing element as closing tag', () => {
|
|
703
|
+
const result = t('<div><span></span></div>')
|
|
704
|
+
expect(result).toContain('_tpl(')
|
|
705
|
+
expect(result).toContain('<span></span>')
|
|
706
706
|
})
|
|
707
707
|
|
|
708
|
-
test(
|
|
709
|
-
const result = t(
|
|
708
|
+
test('handles nested fragment with expression child', () => {
|
|
709
|
+
const result = t('<div><><span />{name()}</></div>')
|
|
710
710
|
// Fragment with expression is inlined, expression with JSX is not present
|
|
711
|
-
expect(result).toContain(
|
|
711
|
+
expect(result).toContain('_tpl(')
|
|
712
712
|
})
|
|
713
713
|
|
|
714
|
-
test(
|
|
715
|
-
const result = t(
|
|
716
|
-
expect(result).toContain(
|
|
717
|
-
expect(result).toContain(
|
|
714
|
+
test('handles fragment with expression containing no JSX', () => {
|
|
715
|
+
const result = t('<div><><span />{count()}</></div>')
|
|
716
|
+
expect(result).toContain('_tpl(')
|
|
717
|
+
expect(result).toContain('_bindText(count,')
|
|
718
718
|
})
|
|
719
719
|
|
|
720
|
-
test(
|
|
721
|
-
const result = t(
|
|
722
|
-
expect(result).toContain(
|
|
723
|
-
expect(result).toContain(
|
|
720
|
+
test('handles nested fragment with text children', () => {
|
|
721
|
+
const result = t('<div><>hello</></div>')
|
|
722
|
+
expect(result).toContain('_tpl(')
|
|
723
|
+
expect(result).toContain('hello')
|
|
724
724
|
})
|
|
725
725
|
|
|
726
|
-
test(
|
|
726
|
+
test('bails on fragment with non-element non-expression child', () => {
|
|
727
727
|
// Fragment containing a component should bail
|
|
728
|
-
const result = t(
|
|
729
|
-
expect(result).not.toContain(
|
|
728
|
+
const result = t('<div><><MyComp /></></div>')
|
|
729
|
+
expect(result).not.toContain('_tpl(')
|
|
730
730
|
})
|
|
731
731
|
|
|
732
|
-
test(
|
|
733
|
-
const result = t(
|
|
734
|
-
expect(result).toContain(
|
|
732
|
+
test('empty expression inside template is handled', () => {
|
|
733
|
+
const result = t('<div><span />{/* comment */}</div>')
|
|
734
|
+
expect(result).toContain('_tpl(')
|
|
735
735
|
})
|
|
736
736
|
|
|
737
|
-
test(
|
|
738
|
-
const result = t(
|
|
739
|
-
expect(result).toContain(
|
|
740
|
-
expect(result).toContain(
|
|
741
|
-
expect(result).toContain(
|
|
737
|
+
test('static expression with multi-expression context uses placeholder', () => {
|
|
738
|
+
const result = t('<div><span>{label}{other}</span></div>')
|
|
739
|
+
expect(result).toContain('_tpl(')
|
|
740
|
+
expect(result).toContain('childNodes[0]')
|
|
741
|
+
expect(result).toContain('childNodes[1]')
|
|
742
742
|
})
|
|
743
743
|
})
|
|
744
744
|
|
|
745
745
|
// ─── Compiler warnings ─────────────────────────────────────────────────────
|
|
746
746
|
|
|
747
|
-
describe(
|
|
748
|
-
test(
|
|
749
|
-
const result = transformJSX(
|
|
747
|
+
describe('JSX transform — warnings', () => {
|
|
748
|
+
test('warns on <For> without by prop', () => {
|
|
749
|
+
const result = transformJSX('<For each={items}>{(item) => <li>{item}</li>}</For>')
|
|
750
750
|
expect(result.warnings).toHaveLength(1)
|
|
751
|
-
expect(result.warnings[0]?.code).toBe(
|
|
751
|
+
expect(result.warnings[0]?.code).toBe('missing-key-on-for')
|
|
752
752
|
expect(result.warnings[0]?.line).toBeGreaterThan(0)
|
|
753
753
|
expect(result.warnings[0]?.column).toBeGreaterThanOrEqual(0)
|
|
754
754
|
})
|
|
755
755
|
|
|
756
|
-
test(
|
|
756
|
+
test('no warning on <For> with by prop', () => {
|
|
757
757
|
const result = transformJSX(
|
|
758
|
-
|
|
758
|
+
'<For each={items} by={(item) => item.id}>{(item) => <li>{item}</li>}</For>',
|
|
759
759
|
)
|
|
760
|
-
const forWarnings = result.warnings.filter((w) => w.code ===
|
|
760
|
+
const forWarnings = result.warnings.filter((w) => w.code === 'missing-key-on-for')
|
|
761
761
|
expect(forWarnings).toHaveLength(0)
|
|
762
762
|
})
|
|
763
763
|
|
|
764
|
-
test(
|
|
765
|
-
const result = transformJSX(
|
|
766
|
-
const forWarnings = result.warnings.filter((w) => w.code ===
|
|
764
|
+
test('no warning on non-For elements without by', () => {
|
|
765
|
+
const result = transformJSX('<div each={items}>{text()}</div>')
|
|
766
|
+
const forWarnings = result.warnings.filter((w) => w.code === 'missing-key-on-for')
|
|
767
767
|
expect(forWarnings).toHaveLength(0)
|
|
768
768
|
})
|
|
769
769
|
})
|
|
770
770
|
|
|
771
771
|
// ─── Hoisting in prop position ──────────────────────────────────────────────
|
|
772
772
|
|
|
773
|
-
describe(
|
|
774
|
-
test(
|
|
775
|
-
const result = t(
|
|
776
|
-
expect(result).toContain(
|
|
777
|
-
expect(result).toContain(
|
|
773
|
+
describe('JSX transform — static JSX attribute hoisting', () => {
|
|
774
|
+
test('hoists static JSX in a DOM element prop', () => {
|
|
775
|
+
const result = t('<div icon={<span>icon</span>} />')
|
|
776
|
+
expect(result).toContain('const _$h0')
|
|
777
|
+
expect(result).toContain('<span>icon</span>')
|
|
778
778
|
})
|
|
779
779
|
})
|
|
780
780
|
|
|
781
781
|
// ─── Additional branch coverage tests ────────────────────────────────────────
|
|
782
782
|
|
|
783
|
-
describe(
|
|
784
|
-
test(
|
|
783
|
+
describe('JSX transform — child expression branches (non-template context)', () => {
|
|
784
|
+
test('wraps dynamic child expression inside a component (non-template path)', () => {
|
|
785
785
|
// Component elements skip template emission, so the child expression
|
|
786
786
|
// goes through the walk() JSX expression handler (lines 195-209)
|
|
787
|
-
const result = t(
|
|
788
|
-
expect(result).toContain(
|
|
787
|
+
const result = t('<MyComponent>{count()}</MyComponent>')
|
|
788
|
+
expect(result).toContain('() => count()')
|
|
789
789
|
})
|
|
790
790
|
|
|
791
|
-
test(
|
|
791
|
+
test('does NOT wrap non-dynamic child expression inside a component', () => {
|
|
792
792
|
// Component context: child expression with no calls — shouldWrap returns false
|
|
793
793
|
// This hits the else branch where neither hoist nor wrap applies (lines 202-204)
|
|
794
|
-
const result = t(
|
|
795
|
-
expect(result).not.toContain(
|
|
796
|
-
expect(result).toContain(
|
|
794
|
+
const result = t('<MyComponent>{someVar}</MyComponent>')
|
|
795
|
+
expect(result).not.toContain('() =>')
|
|
796
|
+
expect(result).toContain('someVar')
|
|
797
797
|
})
|
|
798
798
|
|
|
799
|
-
test(
|
|
799
|
+
test('empty expression in component child is left unchanged', () => {
|
|
800
800
|
// Empty expression (comment) inside component — expr is undefined, line 205-208
|
|
801
|
-
const result = t(
|
|
802
|
-
expect(result).not.toContain(
|
|
801
|
+
const result = t('<MyComponent>{/* comment */}</MyComponent>')
|
|
802
|
+
expect(result).not.toContain('() =>')
|
|
803
803
|
})
|
|
804
804
|
})
|
|
805
805
|
|
|
806
|
-
describe(
|
|
807
|
-
test(
|
|
806
|
+
describe('JSX transform — nested fragment in templateFragmentCount', () => {
|
|
807
|
+
test('handles nested fragment inside fragment in template', () => {
|
|
808
808
|
// This triggers templateFragmentCount being called recursively for nested fragments
|
|
809
809
|
// (lines 318-323)
|
|
810
|
-
const result = t(
|
|
811
|
-
expect(result).toContain(
|
|
812
|
-
expect(result).toContain(
|
|
810
|
+
const result = t('<div><><><span>text</span></></></div>')
|
|
811
|
+
expect(result).toContain('_tpl(')
|
|
812
|
+
expect(result).toContain('<span>text</span>')
|
|
813
813
|
})
|
|
814
814
|
|
|
815
|
-
test(
|
|
815
|
+
test('bails on nested fragment with non-eligible child', () => {
|
|
816
816
|
// Nested fragment containing a component — hits line 325 (return -1)
|
|
817
|
-
const result = t(
|
|
818
|
-
expect(result).not.toContain(
|
|
817
|
+
const result = t('<div><><><MyComp /></></></div>')
|
|
818
|
+
expect(result).not.toContain('_tpl(')
|
|
819
819
|
})
|
|
820
820
|
|
|
821
|
-
test(
|
|
821
|
+
test('nested fragment with expression child in templateFragmentCount', () => {
|
|
822
822
|
// Fragment in fragment with expression — templateFragmentCount handles expression
|
|
823
|
-
const result = t(
|
|
824
|
-
expect(result).toContain(
|
|
825
|
-
expect(result).toContain(
|
|
823
|
+
const result = t('<div><><>{count()}</></></div>')
|
|
824
|
+
expect(result).toContain('_tpl(')
|
|
825
|
+
expect(result).toContain('_bindText(count,')
|
|
826
826
|
})
|
|
827
827
|
|
|
828
|
-
test(
|
|
828
|
+
test('nested fragment with expression containing JSX bails', () => {
|
|
829
829
|
// Fragment in fragment with JSX-containing expression — bails
|
|
830
|
-
const result = t(
|
|
831
|
-
expect(result).not.toContain(
|
|
830
|
+
const result = t('<div><><>{show() && <em />}</></></div>')
|
|
831
|
+
expect(result).not.toContain('_tpl(')
|
|
832
832
|
})
|
|
833
833
|
|
|
834
|
-
test(
|
|
834
|
+
test('nested fragment with empty expression in templateFragmentCount', () => {
|
|
835
835
|
// Fragment in fragment with empty expression (comment)
|
|
836
|
-
const result = t(
|
|
837
|
-
expect(result).toContain(
|
|
836
|
+
const result = t('<div><><>{/* comment */}</></></div>')
|
|
837
|
+
expect(result).toContain('_tpl(')
|
|
838
838
|
})
|
|
839
839
|
})
|
|
840
840
|
|
|
841
|
-
describe(
|
|
842
|
-
test(
|
|
841
|
+
describe('JSX transform — template attribute string expression', () => {
|
|
842
|
+
test('bakes string expression attribute into HTML in template', () => {
|
|
843
843
|
// class={"static"} — string literal in JSX expression → baked into HTML (line 427)
|
|
844
844
|
const result = t('<div class={"static-value"}><span /></div>')
|
|
845
|
-
expect(result).toContain(
|
|
845
|
+
expect(result).toContain('_tpl(')
|
|
846
846
|
expect(result).toContain('class=\\"static-value\\"')
|
|
847
|
-
expect(result).not.toContain(
|
|
847
|
+
expect(result).not.toContain('className')
|
|
848
848
|
})
|
|
849
849
|
|
|
850
|
-
test(
|
|
850
|
+
test('bakes non-class string expression attribute into HTML', () => {
|
|
851
851
|
// title={"hello"} as expression — different attr name (line 427)
|
|
852
852
|
const result = t('<div title={"hello"}><span /></div>')
|
|
853
|
-
expect(result).toContain(
|
|
853
|
+
expect(result).toContain('_tpl(')
|
|
854
854
|
expect(result).toContain('title=\\"hello\\"')
|
|
855
855
|
})
|
|
856
856
|
})
|
|
857
857
|
|
|
858
|
-
describe(
|
|
859
|
-
test(
|
|
858
|
+
describe('JSX transform — one-time className set in template', () => {
|
|
859
|
+
test('one-time className assignment for non-reactive class expression', () => {
|
|
860
860
|
// class={someVar} where someVar has no calls — one-time set (line 450)
|
|
861
|
-
const result = t(
|
|
862
|
-
expect(result).toContain(
|
|
863
|
-
expect(result).toContain(
|
|
864
|
-
expect(result).not.toContain(
|
|
861
|
+
const result = t('<div class={someVar}><span /></div>')
|
|
862
|
+
expect(result).toContain('_tpl(')
|
|
863
|
+
expect(result).toContain('className = someVar')
|
|
864
|
+
expect(result).not.toContain('_bind(')
|
|
865
865
|
})
|
|
866
866
|
})
|
|
867
867
|
|
|
868
|
-
describe(
|
|
869
|
-
test(
|
|
868
|
+
describe('JSX transform — isStaticAttrs edge cases', () => {
|
|
869
|
+
test('static JSX with boolean expression prop is static', () => {
|
|
870
870
|
// Boolean literal in expression: disabled={true} — isStatic returns true
|
|
871
|
-
const result = t(
|
|
872
|
-
expect(result).toContain(
|
|
871
|
+
const result = t('<div>{<input disabled={true} />}</div>')
|
|
872
|
+
expect(result).toContain('const _$h0')
|
|
873
873
|
})
|
|
874
874
|
|
|
875
|
-
test(
|
|
876
|
-
const result = t(
|
|
877
|
-
expect(result).toContain(
|
|
875
|
+
test('static JSX with false expression prop is static', () => {
|
|
876
|
+
const result = t('<div>{<input disabled={false} />}</div>')
|
|
877
|
+
expect(result).toContain('const _$h0')
|
|
878
878
|
})
|
|
879
879
|
|
|
880
|
-
test(
|
|
881
|
-
const result = t(
|
|
882
|
-
expect(result).toContain(
|
|
880
|
+
test('static JSX with null expression prop is static', () => {
|
|
881
|
+
const result = t('<div>{<input disabled={null} />}</div>')
|
|
882
|
+
expect(result).toContain('const _$h0')
|
|
883
883
|
})
|
|
884
884
|
|
|
885
|
-
test(
|
|
886
|
-
const result = t(
|
|
887
|
-
expect(result).toContain(
|
|
885
|
+
test('static JSX with numeric expression prop is static', () => {
|
|
886
|
+
const result = t('<div>{<input tabindex={0} />}</div>')
|
|
887
|
+
expect(result).toContain('const _$h0')
|
|
888
888
|
})
|
|
889
889
|
|
|
890
|
-
test(
|
|
891
|
-
const result = t(
|
|
892
|
-
expect(result).toContain(
|
|
890
|
+
test('static JSX with true expression prop is static', () => {
|
|
891
|
+
const result = t('<div>{<input disabled={true} />}</div>')
|
|
892
|
+
expect(result).toContain('const _$h0')
|
|
893
893
|
})
|
|
894
894
|
|
|
895
|
-
test(
|
|
895
|
+
test('static JSX with empty expression prop is static', () => {
|
|
896
896
|
// Empty expression in attribute: disabled={/* comment */} — expr is undefined
|
|
897
|
-
const result = t(
|
|
898
|
-
expect(result).toContain(
|
|
897
|
+
const result = t('<div>{<input disabled={/* comment */} />}</div>')
|
|
898
|
+
expect(result).toContain('const _$h0')
|
|
899
899
|
})
|
|
900
900
|
})
|
|
901
901
|
|
|
902
|
-
describe(
|
|
903
|
-
test(
|
|
902
|
+
describe('JSX transform — isStaticChild edge cases', () => {
|
|
903
|
+
test('nested static fragment child is recognized as static', () => {
|
|
904
904
|
// Fragment as child of a JSX element being checked for staticness
|
|
905
|
-
const result = t(
|
|
906
|
-
expect(result).toContain(
|
|
905
|
+
const result = t('<div>{<div><>text</></div>}</div>')
|
|
906
|
+
expect(result).toContain('const _$h0')
|
|
907
907
|
})
|
|
908
908
|
|
|
909
|
-
test(
|
|
910
|
-
const result = t(
|
|
911
|
-
expect(result).not.toContain(
|
|
909
|
+
test('nested fragment with dynamic child prevents hoisting', () => {
|
|
910
|
+
const result = t('<div>{<div><>{count()}</></div>}</div>')
|
|
911
|
+
expect(result).not.toContain('const _$h0')
|
|
912
912
|
})
|
|
913
913
|
|
|
914
|
-
test(
|
|
914
|
+
test('expression child in static check — static literal', () => {
|
|
915
915
|
// Expression container with static value inside a JSX node being checked for staticness
|
|
916
916
|
const result = t('<div>{<div>{"hello"}</div>}</div>')
|
|
917
|
-
expect(result).toContain(
|
|
917
|
+
expect(result).toContain('const _$h0')
|
|
918
918
|
})
|
|
919
919
|
|
|
920
|
-
test(
|
|
921
|
-
const result = t(
|
|
922
|
-
expect(result).not.toContain(
|
|
920
|
+
test('expression child in static check — dynamic call prevents hoisting', () => {
|
|
921
|
+
const result = t('<div>{<div>{count()}</div>}</div>')
|
|
922
|
+
expect(result).not.toContain('const _$h0')
|
|
923
923
|
})
|
|
924
924
|
|
|
925
|
-
test(
|
|
926
|
-
const result = t(
|
|
927
|
-
expect(result).toContain(
|
|
925
|
+
test('expression child with empty expression is static', () => {
|
|
926
|
+
const result = t('<div>{<div>{/* comment */}</div>}</div>')
|
|
927
|
+
expect(result).toContain('const _$h0')
|
|
928
928
|
})
|
|
929
929
|
})
|
|
930
930
|
|
|
931
931
|
// ─── Additional branch coverage for 95%+ ──────────────────────────────────────
|
|
932
932
|
|
|
933
|
-
describe(
|
|
934
|
-
test(
|
|
933
|
+
describe('JSX transform — isStaticAttrs boolean shorthand (hoisting path)', () => {
|
|
934
|
+
test('hoists static JSX with boolean shorthand attribute (no initializer)', () => {
|
|
935
935
|
// This triggers isStaticAttrs → !prop.initializer → return true (line 719/2340)
|
|
936
|
-
const result = t(
|
|
937
|
-
expect(result).toContain(
|
|
936
|
+
const result = t('<div>{<input disabled />}</div>')
|
|
937
|
+
expect(result).toContain('const _$h0')
|
|
938
938
|
})
|
|
939
939
|
})
|
|
940
940
|
|
|
941
|
-
describe(
|
|
942
|
-
test(
|
|
941
|
+
describe('JSX transform — isStaticChild with element/self-closing children', () => {
|
|
942
|
+
test('hoists static JSX with self-closing element child', () => {
|
|
943
943
|
// Triggers isStaticChild → isJsxSelfClosingElement path (line 735/2356)
|
|
944
|
-
const result = t(
|
|
945
|
-
expect(result).toContain(
|
|
944
|
+
const result = t('<div>{<div><br /></div>}</div>')
|
|
945
|
+
expect(result).toContain('const _$h0')
|
|
946
946
|
})
|
|
947
947
|
|
|
948
|
-
test(
|
|
948
|
+
test('hoists static JSX with nested element child', () => {
|
|
949
949
|
// Triggers isStaticChild → isJsxElement path (line 736/2357)
|
|
950
|
-
const result = t(
|
|
951
|
-
expect(result).toContain(
|
|
950
|
+
const result = t('<div>{<div><span>text</span></div>}</div>')
|
|
951
|
+
expect(result).toContain('const _$h0')
|
|
952
952
|
})
|
|
953
953
|
|
|
954
|
-
test(
|
|
954
|
+
test('does NOT hoist when nested element child has dynamic props', () => {
|
|
955
955
|
// isStaticChild → isJsxElement → isStaticJSXNode returns false
|
|
956
|
-
const result = t(
|
|
957
|
-
expect(result).not.toContain(
|
|
956
|
+
const result = t('<div>{<div><span class={cls()}>text</span></div>}</div>')
|
|
957
|
+
expect(result).not.toContain('const _$h0')
|
|
958
958
|
})
|
|
959
959
|
|
|
960
|
-
test(
|
|
960
|
+
test('does NOT hoist when self-closing child has dynamic props', () => {
|
|
961
961
|
// isStaticChild → isJsxSelfClosingElement → isStaticJSXNode returns false
|
|
962
|
-
const result = t(
|
|
963
|
-
expect(result).not.toContain(
|
|
962
|
+
const result = t('<div>{<div><input value={val()} /></div>}</div>')
|
|
963
|
+
expect(result).not.toContain('const _$h0')
|
|
964
964
|
})
|
|
965
965
|
})
|
|
966
966
|
|
|
967
|
-
describe(
|
|
968
|
-
test(
|
|
967
|
+
describe('JSX transform — template ref/event without expression', () => {
|
|
968
|
+
test('ref shorthand (no expression) in template is handled', () => {
|
|
969
969
|
// Triggers the else branch of ref initializer check (line 2003)
|
|
970
|
-
const result = t(
|
|
971
|
-
expect(result).toContain(
|
|
972
|
-
expect(result).not.toContain(
|
|
970
|
+
const result = t('<div ref><span /></div>')
|
|
971
|
+
expect(result).toContain('_tpl(')
|
|
972
|
+
expect(result).not.toContain('.current')
|
|
973
973
|
})
|
|
974
974
|
|
|
975
|
-
test(
|
|
975
|
+
test('onClick shorthand (no expression) in template is handled', () => {
|
|
976
976
|
// Triggers the else branch of event initializer check (line 2017)
|
|
977
|
-
const result = t(
|
|
978
|
-
expect(result).toContain(
|
|
979
|
-
expect(result).not.toContain(
|
|
977
|
+
const result = t('<div onClick><span /></div>')
|
|
978
|
+
expect(result).toContain('_tpl(')
|
|
979
|
+
expect(result).not.toContain('addEventListener')
|
|
980
980
|
})
|
|
981
981
|
})
|
|
982
982
|
|
|
983
|
-
describe(
|
|
984
|
-
test(
|
|
983
|
+
describe('JSX transform — empty expression in DOM prop (non-template path)', () => {
|
|
984
|
+
test('empty expression in DOM prop with spread (non-template) is handled', () => {
|
|
985
985
|
// Spread prevents template emission → walk handles attrs
|
|
986
986
|
// class={/* comment */} has no expression → else branch at line 1799
|
|
987
|
-
const result = t(
|
|
988
|
-
expect(result).not.toContain(
|
|
989
|
-
expect(result).toContain(
|
|
987
|
+
const result = t('<div {...props} class={/* comment */} />')
|
|
988
|
+
expect(result).not.toContain('() =>')
|
|
989
|
+
expect(result).toContain('{...props}')
|
|
990
990
|
})
|
|
991
991
|
})
|
|
992
992
|
|
|
993
|
-
describe(
|
|
994
|
-
test(
|
|
993
|
+
describe('JSX transform — whitespace-only text stripped in flattenChildren', () => {
|
|
994
|
+
test('whitespace-only text between elements is stripped in template', () => {
|
|
995
995
|
// Triggers the else branch of `if (trimmed)` in flattenChildren (line 2207)
|
|
996
996
|
const result = t(`<div>
|
|
997
997
|
<span>a</span>
|
|
998
998
|
<em>b</em>
|
|
999
999
|
</div>`)
|
|
1000
|
-
expect(result).toContain(
|
|
1001
|
-
expect(result).toContain(
|
|
1002
|
-
expect(result).toContain(
|
|
1000
|
+
expect(result).toContain('_tpl(')
|
|
1001
|
+
expect(result).toContain('<span>a</span>')
|
|
1002
|
+
expect(result).toContain('<em>b</em>')
|
|
1003
1003
|
})
|
|
1004
1004
|
})
|
|
1005
1005
|
|
|
1006
|
-
describe(
|
|
1007
|
-
test(
|
|
1006
|
+
describe('JSX transform — fragment inside flattenChildren', () => {
|
|
1007
|
+
test('fragment children are flattened during template child processing', () => {
|
|
1008
1008
|
// This specifically exercises the isJsxFragment branch in flattenChildren (line 2223)
|
|
1009
1009
|
// The key is that this fragment is processed via flattenChildren (not templateFragmentCount)
|
|
1010
1010
|
// because the outer element is a template-eligible JsxElement
|
|
1011
|
-
const result = t(
|
|
1012
|
-
expect(result).toContain(
|
|
1013
|
-
expect(result).toContain(
|
|
1014
|
-
expect(result).toContain(
|
|
1011
|
+
const result = t('<div><><span>one</span><em>two</em></></div>')
|
|
1012
|
+
expect(result).toContain('_tpl(')
|
|
1013
|
+
expect(result).toContain('<span>one</span>')
|
|
1014
|
+
expect(result).toContain('<em>two</em>')
|
|
1015
1015
|
})
|
|
1016
1016
|
})
|
|
1017
1017
|
|
|
1018
|
-
describe(
|
|
1019
|
-
test(
|
|
1018
|
+
describe('JSX transform — member expression tag names', () => {
|
|
1019
|
+
test('member expression tag name treated as empty in warnings', () => {
|
|
1020
1020
|
// <ns.Component> has non-identifier tagName → tagName is "" (line 1762)
|
|
1021
|
-
const result = transformJSX(
|
|
1021
|
+
const result = transformJSX('<ns.Comp value={x} />')
|
|
1022
1022
|
expect(result.warnings).toHaveLength(0)
|
|
1023
1023
|
})
|
|
1024
1024
|
|
|
1025
|
-
test(
|
|
1025
|
+
test('member expression tag in element position triggers non-identifier path', () => {
|
|
1026
1026
|
// <ns.div> has a member expression tag → jsxTagName returns "" → templateElementCount returns -1
|
|
1027
|
-
const result = t(
|
|
1027
|
+
const result = t('<ns.div><span /></ns.div>')
|
|
1028
1028
|
// Should not produce template since tagName is not an identifier
|
|
1029
|
-
expect(result).not.toContain(
|
|
1029
|
+
expect(result).not.toContain('_tpl(')
|
|
1030
1030
|
})
|
|
1031
1031
|
})
|
|
1032
1032
|
|
|
1033
1033
|
// ─── Template emission edge cases ─────────────────────────────────────────────
|
|
1034
1034
|
|
|
1035
|
-
describe(
|
|
1036
|
-
test(
|
|
1037
|
-
const result = t(
|
|
1038
|
-
expect(result).toContain(
|
|
1035
|
+
describe('JSX transform — template emission edge cases', () => {
|
|
1036
|
+
test('non-delegated event (onMouseEnter) uses addEventListener not delegation', () => {
|
|
1037
|
+
const result = t('<div onMouseEnter={handler}><span /></div>')
|
|
1038
|
+
expect(result).toContain('_tpl(')
|
|
1039
1039
|
// mouseenter is NOT in DELEGATED_EVENTS → must use addEventListener
|
|
1040
1040
|
// onMouseEnter → eventName = "m" + "ouseEnter" = "mouseEnter"
|
|
1041
|
-
expect(result).toContain(
|
|
1042
|
-
expect(result).toContain(
|
|
1043
|
-
expect(result).not.toContain(
|
|
1041
|
+
expect(result).toContain('addEventListener(')
|
|
1042
|
+
expect(result).toContain('mouseEnter')
|
|
1043
|
+
expect(result).not.toContain('__ev_')
|
|
1044
1044
|
})
|
|
1045
1045
|
|
|
1046
|
-
test(
|
|
1047
|
-
const result = t(
|
|
1048
|
-
expect(result).toContain(
|
|
1046
|
+
test('template with both dynamic attribute AND dynamic child text', () => {
|
|
1047
|
+
const result = t('<div title={getTitle()}>{count()}</div>')
|
|
1048
|
+
expect(result).toContain('_tpl(')
|
|
1049
1049
|
// Dynamic attribute binding
|
|
1050
|
-
expect(result).toContain(
|
|
1050
|
+
expect(result).toContain('_bindDirect(getTitle,')
|
|
1051
1051
|
// Dynamic child text binding
|
|
1052
|
-
expect(result).toContain(
|
|
1052
|
+
expect(result).toContain('_bindText(count,')
|
|
1053
1053
|
})
|
|
1054
1054
|
|
|
1055
|
-
test(
|
|
1056
|
-
const result = t(
|
|
1057
|
-
expect(result).toContain(
|
|
1055
|
+
test('template with multiple dynamic attributes on same element', () => {
|
|
1056
|
+
const result = t('<div class={cls()} title={getTitle()}><span /></div>')
|
|
1057
|
+
expect(result).toContain('_tpl(')
|
|
1058
1058
|
// Both attributes should get _bindDirect bindings
|
|
1059
|
-
expect(result).toContain(
|
|
1060
|
-
expect(result).toContain(
|
|
1059
|
+
expect(result).toContain('_bindDirect(cls,')
|
|
1060
|
+
expect(result).toContain('_bindDirect(getTitle,')
|
|
1061
1061
|
})
|
|
1062
1062
|
|
|
1063
|
-
test(
|
|
1064
|
-
const result = t(
|
|
1065
|
-
expect(result).toContain(
|
|
1066
|
-
expect(result).toContain(
|
|
1067
|
-
expect(result).toContain(
|
|
1063
|
+
test('template with static + dynamic children mixed', () => {
|
|
1064
|
+
const result = t('<div><span>static text</span>{count()}</div>')
|
|
1065
|
+
expect(result).toContain('_tpl(')
|
|
1066
|
+
expect(result).toContain('static text')
|
|
1067
|
+
expect(result).toContain('_bindText(count,')
|
|
1068
1068
|
// Mixed children use childNodes indexing
|
|
1069
|
-
expect(result).toContain(
|
|
1069
|
+
expect(result).toContain('childNodes[')
|
|
1070
1070
|
})
|
|
1071
1071
|
|
|
1072
|
-
test(
|
|
1072
|
+
test('template with nested component inside DOM elements bails', () => {
|
|
1073
1073
|
// Component child inside a DOM element prevents template emission
|
|
1074
|
-
const result = t(
|
|
1075
|
-
expect(result).not.toContain(
|
|
1074
|
+
const result = t('<div><span><MyComponent /></span></div>')
|
|
1075
|
+
expect(result).not.toContain('_tpl(')
|
|
1076
1076
|
})
|
|
1077
1077
|
|
|
1078
|
-
test(
|
|
1079
|
-
const result = t(
|
|
1080
|
-
expect(result).toContain(
|
|
1081
|
-
expect(result).toContain(
|
|
1082
|
-
expect(result).toContain(
|
|
1078
|
+
test('fragment with template-eligible children inside template', () => {
|
|
1079
|
+
const result = t('<div><><span>a</span>{name()}</></div>')
|
|
1080
|
+
expect(result).toContain('_tpl(')
|
|
1081
|
+
expect(result).toContain('<span>a</span>')
|
|
1082
|
+
expect(result).toContain('_bindText(name,')
|
|
1083
1083
|
})
|
|
1084
1084
|
})
|
|
1085
1085
|
|
|
1086
1086
|
// ─── Style attribute handling in templates ───────────────────────────────────
|
|
1087
1087
|
|
|
1088
|
-
describe(
|
|
1089
|
-
test(
|
|
1088
|
+
describe('JSX transform — style attribute in templates', () => {
|
|
1089
|
+
test('style object literal uses Object.assign in _bind', () => {
|
|
1090
1090
|
const result = t('<div style={{ overflow: "hidden" }}>text</div>')
|
|
1091
|
-
expect(result).toContain(
|
|
1092
|
-
expect(result).toContain(
|
|
1091
|
+
expect(result).toContain('_tpl(')
|
|
1092
|
+
expect(result).toContain('Object.assign(__root.style,')
|
|
1093
1093
|
expect(result).toContain('overflow: "hidden"')
|
|
1094
1094
|
})
|
|
1095
1095
|
|
|
1096
|
-
test(
|
|
1096
|
+
test('style string literal inlines as HTML attribute', () => {
|
|
1097
1097
|
const result = t('<div style="color: red">text</div>')
|
|
1098
|
-
expect(result).toContain(
|
|
1098
|
+
expect(result).toContain('_tpl(')
|
|
1099
1099
|
expect(result).toContain('style=\\"color: red\\"')
|
|
1100
1100
|
// Static string should NOT go through _bind
|
|
1101
|
-
expect(result).not.toContain(
|
|
1102
|
-
expect(result).not.toContain(
|
|
1101
|
+
expect(result).not.toContain('Object.assign')
|
|
1102
|
+
expect(result).not.toContain('cssText')
|
|
1103
1103
|
})
|
|
1104
1104
|
|
|
1105
|
-
test(
|
|
1106
|
-
const result = t(
|
|
1107
|
-
expect(result).toContain(
|
|
1108
|
-
expect(result).toContain(
|
|
1105
|
+
test('reactive style uses cssText in _bind', () => {
|
|
1106
|
+
const result = t('<div style={() => getStyle()}>text</div>')
|
|
1107
|
+
expect(result).toContain('_tpl(')
|
|
1108
|
+
expect(result).toContain('style.cssText')
|
|
1109
1109
|
})
|
|
1110
1110
|
})
|