@pyreon/compiler 0.1.0

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