ripple 0.2.208 → 0.2.210

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/package.json +2 -6
  4. package/shims/rollup-estree-types.d.ts +1 -1
  5. package/src/compiler/index.d.ts +1 -0
  6. package/src/compiler/index.js +7 -1
  7. package/src/compiler/phases/1-parse/index.js +15 -6
  8. package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
  9. package/src/compiler/phases/2-analyze/index.js +215 -2
  10. package/src/compiler/phases/3-transform/client/index.js +388 -50
  11. package/src/compiler/phases/3-transform/segments.js +123 -39
  12. package/src/compiler/phases/3-transform/server/index.js +266 -13
  13. package/src/compiler/types/index.d.ts +16 -3
  14. package/src/compiler/utils.js +1 -15
  15. package/src/constants.js +0 -2
  16. package/src/helpers.d.ts +4 -0
  17. package/src/html-tree-validation.js +211 -0
  18. package/src/jsx-runtime.d.ts +260 -259
  19. package/src/jsx-runtime.js +12 -12
  20. package/src/runtime/array.js +17 -17
  21. package/src/runtime/create-subscriber.js +1 -1
  22. package/src/runtime/index-client.js +1 -5
  23. package/src/runtime/index-server.js +15 -0
  24. package/src/runtime/internal/client/compat.js +3 -3
  25. package/src/runtime/internal/client/composite.js +6 -1
  26. package/src/runtime/internal/client/head.js +50 -4
  27. package/src/runtime/internal/client/html.js +73 -12
  28. package/src/runtime/internal/client/hydration.js +12 -0
  29. package/src/runtime/internal/client/index.js +1 -1
  30. package/src/runtime/internal/client/portal.js +54 -29
  31. package/src/runtime/internal/client/rpc.js +3 -1
  32. package/src/runtime/internal/client/switch.js +5 -0
  33. package/src/runtime/internal/client/template.js +117 -11
  34. package/src/runtime/internal/client/try.js +1 -0
  35. package/src/runtime/internal/server/index.js +113 -1
  36. package/src/runtime/internal/server/rpc.js +4 -4
  37. package/src/runtime/map.js +2 -2
  38. package/src/runtime/object.js +6 -6
  39. package/src/runtime/proxy.js +12 -11
  40. package/src/runtime/reactive-value.js +9 -1
  41. package/src/runtime/set.js +12 -7
  42. package/src/runtime/url-search-params.js +0 -1
  43. package/src/server/index.js +4 -0
  44. package/src/utils/hashing.js +15 -0
  45. package/src/utils/normalize_css_property_name.js +1 -1
  46. package/tests/client/array/array.mutations.test.ripple +8 -8
  47. package/tests/client/basic/basic.errors.test.ripple +28 -0
  48. package/tests/client/basic/basic.events.test.ripple +6 -3
  49. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  50. package/tests/client/compiler/compiler.regex.test.ripple +10 -8
  51. package/tests/client/composite/composite.generics.test.ripple +5 -2
  52. package/tests/client/dynamic-elements.test.ripple +30 -1
  53. package/tests/client/function-overload-import.ripple +6 -7
  54. package/tests/client/html.test.ripple +0 -1
  55. package/tests/client/object.test.ripple +2 -2
  56. package/tests/client/portal.test.ripple +3 -3
  57. package/tests/client/return.test.ripple +2500 -0
  58. package/tests/client/try.test.ripple +69 -0
  59. package/tests/client/typescript-generics.test.ripple +1 -1
  60. package/tests/client/url/url.derived.test.ripple +1 -1
  61. package/tests/client/url/url.parsing.test.ripple +3 -3
  62. package/tests/client/url/url.partial-removal.test.ripple +7 -7
  63. package/tests/client/url/url.reactivity.test.ripple +15 -15
  64. package/tests/client/url/url.serialization.test.ripple +2 -2
  65. package/tests/hydration/basic.test.js +23 -0
  66. package/tests/hydration/build-components.js +10 -4
  67. package/tests/hydration/compiled/client/basic.js +165 -3
  68. package/tests/hydration/compiled/client/for.js +1140 -23
  69. package/tests/hydration/compiled/client/head.js +234 -0
  70. package/tests/hydration/compiled/client/html.js +135 -0
  71. package/tests/hydration/compiled/client/portal.js +172 -0
  72. package/tests/hydration/compiled/client/reactivity.js +3 -1
  73. package/tests/hydration/compiled/client/return.js +1976 -0
  74. package/tests/hydration/compiled/client/switch.js +162 -0
  75. package/tests/hydration/compiled/server/basic.js +249 -0
  76. package/tests/hydration/compiled/server/events.js +1 -1
  77. package/tests/hydration/compiled/server/for.js +891 -1
  78. package/tests/hydration/compiled/server/head.js +291 -0
  79. package/tests/hydration/compiled/server/html.js +133 -0
  80. package/tests/hydration/compiled/server/if.js +1 -1
  81. package/tests/hydration/compiled/server/portal.js +250 -0
  82. package/tests/hydration/compiled/server/reactivity.js +1 -1
  83. package/tests/hydration/compiled/server/return.js +1969 -0
  84. package/tests/hydration/compiled/server/switch.js +130 -0
  85. package/tests/hydration/components/basic.ripple +55 -0
  86. package/tests/hydration/components/for.ripple +403 -0
  87. package/tests/hydration/components/head.ripple +111 -0
  88. package/tests/hydration/components/html.ripple +38 -0
  89. package/tests/hydration/components/portal.ripple +49 -0
  90. package/tests/hydration/components/return.ripple +564 -0
  91. package/tests/hydration/components/switch.ripple +51 -0
  92. package/tests/hydration/for.test.js +363 -0
  93. package/tests/hydration/head.test.js +105 -0
  94. package/tests/hydration/html.test.js +46 -0
  95. package/tests/hydration/portal.test.js +71 -0
  96. package/tests/hydration/return.test.js +544 -0
  97. package/tests/hydration/switch.test.js +42 -0
  98. package/tests/server/basic.attributes.test.ripple +1 -1
  99. package/tests/server/compiler.test.ripple +22 -0
  100. package/tests/server/composite.test.ripple +5 -2
  101. package/tests/server/html-nesting-validation.test.ripple +237 -0
  102. package/tests/server/return.test.ripple +1379 -0
  103. package/tests/setup-hydration.js +6 -1
  104. package/tests/utils/escaping.test.js +3 -1
  105. package/tests/utils/normalize_css_property_name.test.js +0 -1
  106. package/tests/utils/patterns.test.js +6 -2
  107. package/tests/utils/sanitize_template_string.test.js +3 -2
  108. package/types/server.d.ts +16 -0
@@ -0,0 +1,1379 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from 'ripple/server';
3
+
4
+ describe('early return in SSR', () => {
5
+ it('skips template content after direct return', async () => {
6
+ component App() {
7
+ <div>{'before'}</div>
8
+ return;
9
+ <div>{'after'}</div>
10
+ }
11
+
12
+ const { body } = await render(App);
13
+ expect(body).toBeHtml('<div>before</div>');
14
+ });
15
+
16
+ it('skips rest of body when return condition is true', async () => {
17
+ component App() {
18
+ let condition = true;
19
+
20
+ if (condition) {
21
+ <div>{'guard hit'}</div>
22
+ return;
23
+ }
24
+ <div>{'rest'}</div>
25
+ }
26
+
27
+ const { body } = await render(App);
28
+ expect(body).toBeHtml('<div>guard hit</div>');
29
+ });
30
+
31
+ it('renders rest of body when return condition is false', async () => {
32
+ component App() {
33
+ let condition = false;
34
+
35
+ if (condition) {
36
+ <div>{'guard hit'}</div>
37
+ return;
38
+ }
39
+ <div>{'rest'}</div>
40
+ }
41
+
42
+ const { body } = await render(App);
43
+ expect(body).toBeHtml('<div>rest</div>');
44
+ });
45
+
46
+ it('emits hydration markers for content guarded by early return flags', async () => {
47
+ component App() {
48
+ let condition = true;
49
+
50
+ if (condition) {
51
+ return;
52
+ }
53
+
54
+ <div>{'rest'}</div>
55
+ }
56
+
57
+ const { body } = await render(App);
58
+ const open_markers = body.match(/<!--\[-->/g) || [];
59
+ const close_markers = body.match(/<!--\]-->/g) || [];
60
+ expect(open_markers.length).toBeGreaterThanOrEqual(3);
61
+ expect(close_markers.length).toBeGreaterThanOrEqual(3);
62
+ expect(open_markers.length).toBe(close_markers.length);
63
+ expect(body).toBeHtml('');
64
+ });
65
+
66
+ it('handles nested ifs with return', async () => {
67
+ component App() {
68
+ let a = true;
69
+ let b = true;
70
+
71
+ if (a) {
72
+ <div>{'a is true'}</div>
73
+ if (b) {
74
+ <div>{'b is true'}</div>
75
+ return;
76
+ }
77
+ }
78
+ <div>{'rest renders only when !(a && b)'}</div>
79
+ }
80
+
81
+ const { body } = await render(App);
82
+ expect(body).toBeHtml('<div>a is true</div><div>b is true</div>');
83
+ });
84
+
85
+ it('renders rest when nested return condition is not fully met', async () => {
86
+ component App() {
87
+ let a = true;
88
+ let b = false;
89
+
90
+ if (a) {
91
+ <div>{'a is true'}</div>
92
+ if (b) {
93
+ <div>{'b is true'}</div>
94
+ return;
95
+ }
96
+ }
97
+ <div>{'rest'}</div>
98
+ }
99
+
100
+ const { body } = await render(App);
101
+ expect(body).toBeHtml('<div>a is true</div><div>rest</div>');
102
+ });
103
+
104
+ it('renders rest when outer condition is false', async () => {
105
+ component App() {
106
+ let a = false;
107
+ let b = true;
108
+
109
+ if (a) {
110
+ <div>{'a is true'}</div>
111
+ if (b) {
112
+ <div>{'b is true'}</div>
113
+ return;
114
+ }
115
+ }
116
+ <div>{'rest'}</div>
117
+ }
118
+
119
+ const { body } = await render(App);
120
+ expect(body).toBeHtml('<div>rest</div>');
121
+ });
122
+
123
+ it('handles content before and after the if-with-return', async () => {
124
+ component App() {
125
+ let shouldReturn = true;
126
+
127
+ <div>{'before'}</div>
128
+ if (shouldReturn) {
129
+ <div>{'guard'}</div>
130
+ return;
131
+ }
132
+ <div>{'after'}</div>
133
+ }
134
+
135
+ const { body } = await render(App);
136
+ expect(body).toBeHtml('<div>before</div><div>guard</div>');
137
+ });
138
+
139
+ it('renders multiple elements after guard when condition is false', async () => {
140
+ component App() {
141
+ let shouldReturn = false;
142
+
143
+ if (shouldReturn) {
144
+ <div>{'guard'}</div>
145
+ return;
146
+ }
147
+ <div>{'first'}</div>
148
+ <div>{'second'}</div>
149
+ }
150
+
151
+ const { body } = await render(App);
152
+ expect(body).toBeHtml('<div>first</div><div>second</div>');
153
+ });
154
+
155
+ it('handles multiple sequential returns - first hits', async () => {
156
+ component App() {
157
+ let a = true;
158
+ let b = true;
159
+
160
+ if (a) {
161
+ <div>{'first guard'}</div>
162
+ return;
163
+ }
164
+ if (b) {
165
+ <div>{'second guard'}</div>
166
+ return;
167
+ }
168
+ <div>{'rest'}</div>
169
+ }
170
+
171
+ const { body } = await render(App);
172
+ expect(body).toBeHtml('<div>first guard</div>');
173
+ });
174
+
175
+ it('handles multiple sequential returns - second hits', async () => {
176
+ component App() {
177
+ let a = false;
178
+ let b = true;
179
+
180
+ if (a) {
181
+ <div>{'first guard'}</div>
182
+ return;
183
+ }
184
+ if (b) {
185
+ <div>{'second guard'}</div>
186
+ return;
187
+ }
188
+ <div>{'rest'}</div>
189
+ }
190
+
191
+ const { body } = await render(App);
192
+ expect(body).toBeHtml('<div>second guard</div>');
193
+ });
194
+
195
+ it('handles multiple sequential returns - none hit', async () => {
196
+ component App() {
197
+ let a = false;
198
+ let b = false;
199
+
200
+ if (a) {
201
+ <div>{'first guard'}</div>
202
+ return;
203
+ }
204
+ if (b) {
205
+ <div>{'second guard'}</div>
206
+ return;
207
+ }
208
+ <div>{'rest'}</div>
209
+ }
210
+
211
+ const { body } = await render(App);
212
+ expect(body).toBeHtml('<div>rest</div>');
213
+ });
214
+
215
+ it('handles deeply nested returns (3 levels)', async () => {
216
+ component App() {
217
+ let a = true;
218
+ let b = true;
219
+ let c = true;
220
+
221
+ if (a) {
222
+ <div>{'a'}</div>
223
+ if (b) {
224
+ <div>{'b'}</div>
225
+ if (c) {
226
+ <div>{'c'}</div>
227
+ return;
228
+ }
229
+ }
230
+ }
231
+ <div>{'rest'}</div>
232
+ }
233
+
234
+ const { body } = await render(App);
235
+ expect(body).toBeHtml('<div>a</div><div>b</div><div>c</div>');
236
+ });
237
+
238
+ it('handles deeply nested returns (3 levels) - partial', async () => {
239
+ component App() {
240
+ let a = true;
241
+ let b = true;
242
+ let c = false;
243
+
244
+ if (a) {
245
+ <div>{'a'}</div>
246
+ if (b) {
247
+ <div>{'b'}</div>
248
+ if (c) {
249
+ <div>{'c'}</div>
250
+ return;
251
+ }
252
+ }
253
+ }
254
+ <div>{'rest'}</div>
255
+ }
256
+
257
+ const { body } = await render(App);
258
+ expect(body).toBeHtml('<div>a</div><div>b</div><div>rest</div>');
259
+ });
260
+
261
+ it('handles return with else-if chain - first condition', async () => {
262
+ component App() {
263
+ let value = 1;
264
+
265
+ if (value === 1) {
266
+ <div>{'one'}</div>
267
+ return;
268
+ } else if (value === 2) {
269
+ <div>{'two'}</div>
270
+ return;
271
+ } else {
272
+ <div>{'other'}</div>
273
+ return;
274
+ }
275
+ <div>{'never reached'}</div>
276
+ }
277
+
278
+ const { body } = await render(App);
279
+ expect(body).toBeHtml('<div>one</div>');
280
+ });
281
+
282
+ it('handles return with else-if chain - second condition', async () => {
283
+ component App() {
284
+ let value = 2;
285
+
286
+ if (value === 1) {
287
+ <div>{'one'}</div>
288
+ return;
289
+ } else if (value === 2) {
290
+ <div>{'two'}</div>
291
+ return;
292
+ } else {
293
+ <div>{'other'}</div>
294
+ return;
295
+ }
296
+ <div>{'never reached'}</div>
297
+ }
298
+
299
+ const { body } = await render(App);
300
+ expect(body).toBeHtml('<div>two</div>');
301
+ });
302
+
303
+ it('handles return with else-if chain - else condition', async () => {
304
+ component App() {
305
+ let value = 3;
306
+
307
+ if (value === 1) {
308
+ <div>{'one'}</div>
309
+ return;
310
+ } else if (value === 2) {
311
+ <div>{'two'}</div>
312
+ return;
313
+ } else {
314
+ <div>{'other'}</div>
315
+ return;
316
+ }
317
+ <div>{'never reached'}</div>
318
+ }
319
+
320
+ const { body } = await render(App);
321
+ expect(body).toBeHtml('<div>other</div>');
322
+ });
323
+
324
+ it('handles return with complex boolean expression - AND', async () => {
325
+ component App() {
326
+ let a = true;
327
+ let b = true;
328
+ let c = true;
329
+
330
+ if (a && b && c) {
331
+ <div>{'all true'}</div>
332
+ return;
333
+ }
334
+ <div>{'not all true'}</div>
335
+ }
336
+
337
+ const { body } = await render(App);
338
+ expect(body).toBeHtml('<div>all true</div>');
339
+ });
340
+
341
+ it('handles return with complex boolean expression - OR', async () => {
342
+ component App() {
343
+ let a = false;
344
+ let b = true;
345
+ let c = false;
346
+
347
+ if (a || b || c) {
348
+ <div>{'at least one true'}</div>
349
+ return;
350
+ }
351
+ <div>{'all false'}</div>
352
+ }
353
+
354
+ const { body } = await render(App);
355
+ expect(body).toBeHtml('<div>at least one true</div>');
356
+ });
357
+
358
+ it('handles return with complex boolean expression - mixed', async () => {
359
+ component App() {
360
+ let a = true;
361
+ let b = false;
362
+ let c = true;
363
+
364
+ if (a && !b || c) {
365
+ <div>{'complex condition met'}</div>
366
+ return;
367
+ }
368
+ <div>{'complex condition not met'}</div>
369
+ }
370
+
371
+ const { body } = await render(App);
372
+ expect(body).toBeHtml('<div>complex condition met</div>');
373
+ });
374
+
375
+ it('handles return with numeric comparison', async () => {
376
+ component App() {
377
+ let num = 10;
378
+
379
+ if (num > 5) {
380
+ <div>{'greater than 5'}</div>
381
+ return;
382
+ }
383
+ <div>{'5 or less'}</div>
384
+ }
385
+
386
+ const { body } = await render(App);
387
+ expect(body).toBeHtml('<div>greater than 5</div>');
388
+ });
389
+
390
+ it('handles return with string comparison', async () => {
391
+ component App() {
392
+ let str = 'hello';
393
+
394
+ if (str === 'hello') {
395
+ <div>{'greeting'}</div>
396
+ return;
397
+ }
398
+ <div>{'not greeting'}</div>
399
+ }
400
+
401
+ const { body } = await render(App);
402
+ expect(body).toBeHtml('<div>greeting</div>');
403
+ });
404
+
405
+ it('handles return with negated condition', async () => {
406
+ component App() {
407
+ let flag = false;
408
+
409
+ if (!flag) {
410
+ <div>{'flag is false'}</div>
411
+ return;
412
+ }
413
+ <div>{'flag is true'}</div>
414
+ }
415
+
416
+ const { body } = await render(App);
417
+ expect(body).toBeHtml('<div>flag is false</div>');
418
+ });
419
+
420
+ it('handles return with ternary result', async () => {
421
+ component App() {
422
+ let condition = true;
423
+
424
+ if (condition) {
425
+ <div>{condition ? 'yes' : 'no'}</div>
426
+ return;
427
+ }
428
+ <div>{'fallback'}</div>
429
+ }
430
+
431
+ const { body } = await render(App);
432
+ expect(body).toBeHtml('<div>yes</div>');
433
+ });
434
+
435
+ it('handles return in nested component scope', async () => {
436
+ component App() {
437
+ let show = true;
438
+
439
+ <div>
440
+ <span>{'outer'}</span>
441
+ if (show) {
442
+ <p>{'inner'}</p>
443
+ return;
444
+ }
445
+ <p>{'after'}</p>
446
+ </div>
447
+ }
448
+
449
+ const { body } = await render(App);
450
+ expect(body).toBeHtml('<div><span>outer</span><p>inner</p></div>');
451
+ });
452
+
453
+ it('handles return with multiple elements before and after', async () => {
454
+ component App() {
455
+ let shouldReturn = true;
456
+
457
+ <h1>{'title'}</h1>
458
+ <p>{'description'}</p>
459
+ if (shouldReturn) {
460
+ <div>{'guard'}</div>
461
+ <span>{'guard span'}</span>
462
+ return;
463
+ }
464
+ <footer>{'footer'}</footer>
465
+ <nav>{'nav'}</nav>
466
+ }
467
+
468
+ const { body } = await render(App);
469
+ expect(body).toBeHtml(
470
+ '<h1>title</h1><p>description</p><div>guard</div><span>guard span</span>',
471
+ );
472
+ });
473
+
474
+ it('handles return at the beginning of component', async () => {
475
+ component App() {
476
+ if (true) {
477
+ <div>{'early exit'}</div>
478
+ return;
479
+ }
480
+ <div>{'never reached'}</div>
481
+ <div>{'also never reached'}</div>
482
+ }
483
+
484
+ const { body } = await render(App);
485
+ expect(body).toBeHtml('<div>early exit</div>');
486
+ });
487
+
488
+ it('handles return at the end of component', async () => {
489
+ component App() {
490
+ <div>{'first'}</div>
491
+ <div>{'second'}</div>
492
+ if (true) {
493
+ <div>{'third'}</div>
494
+ return;
495
+ }
496
+ }
497
+
498
+ const { body } = await render(App);
499
+ expect(body).toBeHtml('<div>first</div><div>second</div><div>third</div>');
500
+ });
501
+
502
+ it('handles return with empty elements before (self-closing div)', async () => {
503
+ component App() {
504
+ <div />
505
+ if (true) {
506
+ <span>{'content'}</span>
507
+ return;
508
+ }
509
+ <p>{'after'}</p>
510
+ }
511
+
512
+ const { body } = await render(App);
513
+ expect(body).toBeHtml('<div></div><span>content</span>');
514
+ });
515
+
516
+ it('handles return with function call in condition', async () => {
517
+ component App() {
518
+ function check() {
519
+ return true;
520
+ }
521
+
522
+ if (check()) {
523
+ <div>{'function returned true'}</div>
524
+ return;
525
+ }
526
+ <div>{'function returned false'}</div>
527
+ }
528
+
529
+ const { body } = await render(App);
530
+ expect(body).toBeHtml('<div>function returned true</div>');
531
+ });
532
+
533
+ it('handles return with arithmetic in condition (corrected: 5+3=8 > 7)', async () => {
534
+ component App() {
535
+ let x = 5;
536
+ let y = 3;
537
+
538
+ if (x + y > 7) {
539
+ <div>{'sum greater than 7'}</div>
540
+ return;
541
+ }
542
+ <div>{'sum 7 or less'}</div>
543
+ }
544
+
545
+ const { body } = await render(App);
546
+ expect(body).toBeHtml('<div>sum greater than 7</div>');
547
+ });
548
+
549
+ it('handles multiple sibling returns at same level', async () => {
550
+ component App() {
551
+ let mode = 'b';
552
+
553
+ if (mode === 'a') {
554
+ <div>{'mode A'}</div>
555
+ return;
556
+ }
557
+
558
+ if (mode === 'b') {
559
+ <div>{'mode B'}</div>
560
+ return;
561
+ }
562
+
563
+ if (mode === 'c') {
564
+ <div>{'mode C'}</div>
565
+ return;
566
+ }
567
+
568
+ <div>{'default mode'}</div>
569
+ }
570
+
571
+ const { body } = await render(App);
572
+ expect(body).toBeHtml('<div>mode B</div>');
573
+ });
574
+
575
+ it('handles return with array length check', async () => {
576
+ component App() {
577
+ let items = [1, 2, 3];
578
+
579
+ if (items.length > 0) {
580
+ <div>{'has items'}</div>
581
+ return;
582
+ }
583
+ <div>{'empty'}</div>
584
+ }
585
+
586
+ const { body } = await render(App);
587
+ expect(body).toBeHtml('<div>has items</div>');
588
+ });
589
+
590
+ it('handles return with object property check', async () => {
591
+ component App() {
592
+ let obj = { value: 42 };
593
+
594
+ if (obj.value === 42) {
595
+ <div>{'correct value'}</div>
596
+ return;
597
+ }
598
+ <div>{'wrong value'}</div>
599
+ }
600
+
601
+ const { body } = await render(App);
602
+ expect(body).toBeHtml('<div>correct value</div>');
603
+ });
604
+
605
+ it('handles return with typeof check', async () => {
606
+ component App() {
607
+ let value = 'string';
608
+
609
+ if (typeof value === 'string') {
610
+ <div>{'is string'}</div>
611
+ return;
612
+ }
613
+ <div>{'not string'}</div>
614
+ }
615
+
616
+ const { body } = await render(App);
617
+ expect(body).toBeHtml('<div>is string</div>');
618
+ });
619
+
620
+ it('handles return with null check', async () => {
621
+ component App() {
622
+ let value = null;
623
+
624
+ if (value === null) {
625
+ <div>{'is null'}</div>
626
+ return;
627
+ }
628
+ <div>{'not null'}</div>
629
+ }
630
+
631
+ const { body } = await render(App);
632
+ expect(body).toBeHtml('<div>is null</div>');
633
+ });
634
+
635
+ it('handles return with undefined check', async () => {
636
+ component App() {
637
+ let value = undefined;
638
+
639
+ if (value === undefined) {
640
+ <div>{'is undefined'}</div>
641
+ return;
642
+ }
643
+ <div>{'not undefined'}</div>
644
+ }
645
+
646
+ const { body } = await render(App);
647
+ expect(body).toBeHtml('<div>is undefined</div>');
648
+ });
649
+
650
+ it('handles return with truthy/falsy values', async () => {
651
+ component App() {
652
+ let value = 0;
653
+
654
+ if (value) {
655
+ <div>{'truthy'}</div>
656
+ return;
657
+ }
658
+ <div>{'falsy'}</div>
659
+ }
660
+
661
+ const { body } = await render(App);
662
+ expect(body).toBeHtml('<div>falsy</div>');
663
+ });
664
+
665
+ it('handles return with empty string check', async () => {
666
+ component App() {
667
+ let str = '';
668
+
669
+ if (str === '') {
670
+ <div>{'empty string'}</div>
671
+ return;
672
+ }
673
+ <div>{'non-empty string'}</div>
674
+ }
675
+
676
+ const { body } = await render(App);
677
+ expect(body).toBeHtml('<div>empty string</div>');
678
+ });
679
+
680
+ it('handles return with instance check', async () => {
681
+ component App() {
682
+ let arr = [1, 2, 3];
683
+
684
+ if (arr instanceof Array) {
685
+ <div>{'is array'}</div>
686
+ return;
687
+ }
688
+ <div>{'not array'}</div>
689
+ }
690
+
691
+ const { body } = await render(App);
692
+ expect(body).toBeHtml('<div>is array</div>');
693
+ });
694
+
695
+ it('handles return with in operator', async () => {
696
+ component App() {
697
+ let obj = { a: 1, b: 2 };
698
+
699
+ if ('a' in obj) {
700
+ <div>{'has property a'}</div>
701
+ return;
702
+ }
703
+ <div>{'no property a'}</div>
704
+ }
705
+
706
+ const { body } = await render(App);
707
+ expect(body).toBeHtml('<div>has property a</div>');
708
+ });
709
+
710
+ it('handles return with switch-like pattern using else-if', async () => {
711
+ component App() {
712
+ let status = 'loading';
713
+
714
+ if (status === 'idle') {
715
+ <div>{'idle state'}</div>
716
+ return;
717
+ } else if (status === 'loading') {
718
+ <div>{'loading state'}</div>
719
+ return;
720
+ } else if (status === 'success') {
721
+ <div>{'success state'}</div>
722
+ return;
723
+ } else if (status === 'error') {
724
+ <div>{'error state'}</div>
725
+ return;
726
+ } else {
727
+ <div>{'unknown state'}</div>
728
+ return;
729
+ }
730
+ <div>{'never reached'}</div>
731
+ }
732
+
733
+ const { body } = await render(App);
734
+ expect(body).toBeHtml('<div>loading state</div>');
735
+ });
736
+
737
+ it('handles return with multiple nested levels and mixed conditions', async () => {
738
+ component App() {
739
+ let a = true;
740
+ let b = false;
741
+ let c = true;
742
+ let d = true;
743
+
744
+ if (a) {
745
+ <div>{'a'}</div>
746
+ if (b) {
747
+ <div>{'b'}</div>
748
+ return;
749
+ }
750
+ if (c) {
751
+ <div>{'c'}</div>
752
+ if (d) {
753
+ <div>{'d'}</div>
754
+ return;
755
+ }
756
+ }
757
+ }
758
+ <div>{'rest'}</div>
759
+ }
760
+
761
+ const { body } = await render(App);
762
+ expect(body).toBeHtml('<div>a</div><div>c</div><div>d</div>');
763
+ });
764
+
765
+ it('handles return with conditional rendering before guard', async () => {
766
+ component App() {
767
+ let showHeader = true;
768
+ let shouldReturn = true;
769
+
770
+ if (showHeader) {
771
+ <h1>{'Header'}</h1>
772
+ }
773
+ if (shouldReturn) {
774
+ <div>{'guard'}</div>
775
+ return;
776
+ }
777
+ <footer>{'Footer'}</footer>
778
+ }
779
+
780
+ const { body } = await render(App);
781
+ expect(body).toBeHtml('<h1>Header</h1><div>guard</div>');
782
+ });
783
+
784
+ it('handles return with else branch that does not return', async () => {
785
+ component App() {
786
+ let condition = false;
787
+
788
+ if (condition) {
789
+ <div>{'condition true'}</div>
790
+ return;
791
+ } else {
792
+ <div>{'condition false'}</div>
793
+ }
794
+ <div>{'after if-else'}</div>
795
+ }
796
+
797
+ const { body } = await render(App);
798
+ expect(body).toBeHtml('<div>condition false</div><div>after if-else</div>');
799
+ });
800
+
801
+ it('handles return with else branch that also returns', async () => {
802
+ component App() {
803
+ let condition = false;
804
+
805
+ if (condition) {
806
+ <div>{'condition true'}</div>
807
+ return;
808
+ } else {
809
+ <div>{'condition false'}</div>
810
+ return;
811
+ }
812
+ <div>{'never reached'}</div>
813
+ }
814
+
815
+ const { body } = await render(App);
816
+ expect(body).toBeHtml('<div>condition false</div>');
817
+ });
818
+
819
+ it('handles return with only if branch returning', async () => {
820
+ component App() {
821
+ let condition = false;
822
+
823
+ if (condition) {
824
+ <div>{'condition true'}</div>
825
+ return;
826
+ }
827
+ <div>{'condition false or after'}</div>
828
+ }
829
+
830
+ const { body } = await render(App);
831
+ expect(body).toBeHtml('<div>condition false or after</div>');
832
+ });
833
+
834
+ it('handles return with deeply nested else-if chain', async () => {
835
+ component App() {
836
+ let x = 1;
837
+ let y = 2;
838
+
839
+ if (x === 1) {
840
+ if (y === 1) {
841
+ <div>{'x=1, y=1'}</div>
842
+ return;
843
+ } else if (y === 2) {
844
+ <div>{'x=1, y=2'}</div>
845
+ return;
846
+ } else {
847
+ <div>{'x=1, y=other'}</div>
848
+ return;
849
+ }
850
+ }
851
+ <div>{'x!=1'}</div>
852
+ }
853
+
854
+ const { body } = await render(App);
855
+ expect(body).toBeHtml('<div>x=1, y=2</div>');
856
+ });
857
+
858
+ describe('nested return scenarios', () => {
859
+ it('nested return hides content after inner if inside outer if', async () => {
860
+ component App() {
861
+ let a = true;
862
+ let b = true;
863
+
864
+ if (a) {
865
+ <div>{'a'}</div>
866
+ if (b) {
867
+ <div>{'b'}</div>
868
+ return;
869
+ }
870
+ <div>{'after inner'}</div>
871
+ }
872
+ <div>{'rest'}</div>
873
+ }
874
+
875
+ const { body } = await render(App);
876
+ expect(body).toBeHtml('<div>a</div><div>b</div>');
877
+ });
878
+
879
+ it('nested return shows content after inner if when inner condition is false', async () => {
880
+ component App() {
881
+ let a = true;
882
+ let b = false;
883
+
884
+ if (a) {
885
+ <div>{'a'}</div>
886
+ if (b) {
887
+ <div>{'b'}</div>
888
+ return;
889
+ }
890
+ <div>{'after inner'}</div>
891
+ }
892
+ <div>{'rest'}</div>
893
+ }
894
+
895
+ const { body } = await render(App);
896
+ expect(body).toBeHtml('<div>a</div><div>after inner</div><div>rest</div>');
897
+ });
898
+
899
+ it('nested return with sibling returns inside outer if', async () => {
900
+ component App() {
901
+ let outer = true;
902
+ let a = false;
903
+ let b = true;
904
+
905
+ if (outer) {
906
+ <div>{'outer'}</div>
907
+ if (a) {
908
+ <div>{'a'}</div>
909
+ return;
910
+ }
911
+ <div>{'between'}</div>
912
+ if (b) {
913
+ <div>{'b'}</div>
914
+ return;
915
+ }
916
+ <div>{'after b'}</div>
917
+ }
918
+ <div>{'rest'}</div>
919
+ }
920
+
921
+ const { body } = await render(App);
922
+ expect(body).toBeHtml('<div>outer</div><div>between</div><div>b</div>');
923
+ });
924
+
925
+ it('nested return inside else branch', async () => {
926
+ component App() {
927
+ let a = false;
928
+ let b = true;
929
+
930
+ if (a) {
931
+ <div>{'a'}</div>
932
+ } else {
933
+ <div>{'else'}</div>
934
+ if (b) {
935
+ <div>{'b'}</div>
936
+ return;
937
+ }
938
+ }
939
+ <div>{'rest'}</div>
940
+ }
941
+
942
+ const { body } = await render(App);
943
+ expect(body).toBeHtml('<div>else</div><div>b</div>');
944
+ });
945
+
946
+ it('deeply nested returns (4 levels) - all true', async () => {
947
+ component App() {
948
+ let a = true;
949
+ let b = true;
950
+ let c = true;
951
+ let d = true;
952
+
953
+ if (a) {
954
+ <div>{'a'}</div>
955
+ if (b) {
956
+ <div>{'b'}</div>
957
+ if (c) {
958
+ <div>{'c'}</div>
959
+ if (d) {
960
+ <div>{'d'}</div>
961
+ return;
962
+ }
963
+ }
964
+ }
965
+ }
966
+ <div>{'rest'}</div>
967
+ }
968
+
969
+ const { body } = await render(App);
970
+ expect(body).toBeHtml('<div>a</div><div>b</div><div>c</div><div>d</div>');
971
+ });
972
+
973
+ it('deeply nested returns (4 levels) - innermost false', async () => {
974
+ component App() {
975
+ let a = true;
976
+ let b = true;
977
+ let c = true;
978
+ let d = false;
979
+
980
+ if (a) {
981
+ <div>{'a'}</div>
982
+ if (b) {
983
+ <div>{'b'}</div>
984
+ if (c) {
985
+ <div>{'c'}</div>
986
+ if (d) {
987
+ <div>{'d'}</div>
988
+ return;
989
+ }
990
+ }
991
+ }
992
+ }
993
+ <div>{'rest'}</div>
994
+ }
995
+
996
+ const { body } = await render(App);
997
+ expect(body).toBeHtml('<div>a</div><div>b</div><div>c</div><div>rest</div>');
998
+ });
999
+
1000
+ it('nested return with else at outer level', async () => {
1001
+ component App() {
1002
+ let a = true;
1003
+ let b = true;
1004
+
1005
+ if (a) {
1006
+ <div>{'a'}</div>
1007
+ if (b) {
1008
+ <div>{'b'}</div>
1009
+ return;
1010
+ }
1011
+ } else {
1012
+ <div>{'else'}</div>
1013
+ }
1014
+ <div>{'rest'}</div>
1015
+ }
1016
+
1017
+ const { body } = await render(App);
1018
+ expect(body).toBeHtml('<div>a</div><div>b</div>');
1019
+ });
1020
+
1021
+ it('nested return with else at outer level - outer false', async () => {
1022
+ component App() {
1023
+ let a = false;
1024
+ let b = true;
1025
+
1026
+ if (a) {
1027
+ <div>{'a'}</div>
1028
+ if (b) {
1029
+ <div>{'b'}</div>
1030
+ return;
1031
+ }
1032
+ } else {
1033
+ <div>{'else'}</div>
1034
+ }
1035
+ <div>{'rest'}</div>
1036
+ }
1037
+
1038
+ const { body } = await render(App);
1039
+ expect(body).toBeHtml('<div>else</div><div>rest</div>');
1040
+ });
1041
+
1042
+ it('nested return hides content at multiple intermediate levels', async () => {
1043
+ component App() {
1044
+ let a = true;
1045
+ let b = true;
1046
+ let c = true;
1047
+
1048
+ if (a) {
1049
+ <div>{'a'}</div>
1050
+ if (b) {
1051
+ <div>{'b'}</div>
1052
+ if (c) {
1053
+ <div>{'c'}</div>
1054
+ return;
1055
+ }
1056
+ <div>{'after c'}</div>
1057
+ }
1058
+ <div>{'after b'}</div>
1059
+ }
1060
+ <div>{'rest'}</div>
1061
+ }
1062
+
1063
+ const { body } = await render(App);
1064
+ expect(body).toBeHtml('<div>a</div><div>b</div><div>c</div>');
1065
+ });
1066
+ });
1067
+
1068
+ it('handles return with comparison operators', async () => {
1069
+ component App() {
1070
+ let a = 5;
1071
+ let b = 10;
1072
+
1073
+ if (a < b) {
1074
+ <div>{'a less than b'}</div>
1075
+ return;
1076
+ }
1077
+ <div>{'a not less than b'}</div>
1078
+ }
1079
+
1080
+ const { body } = await render(App);
1081
+ expect(body).toBeHtml('<div>a less than b</div>');
1082
+ });
1083
+
1084
+ it('handles return with equality operators', async () => {
1085
+ component App() {
1086
+ let a = 5;
1087
+ let b = '5';
1088
+
1089
+ // @ts-expect-error testing loose equality
1090
+ if (a == b) {
1091
+ <div>{'loose equality'}</div>
1092
+ return;
1093
+ }
1094
+ <div>{'not loosely equal'}</div>
1095
+ }
1096
+
1097
+ const { body } = await render(App);
1098
+ expect(body).toBeHtml('<div>loose equality</div>');
1099
+ });
1100
+
1101
+ it('handles return with strict equality', async () => {
1102
+ component App() {
1103
+ let a = 5;
1104
+ let b = 5;
1105
+
1106
+ if (a === b) {
1107
+ <div>{'strict equality'}</div>
1108
+ return;
1109
+ }
1110
+ <div>{'not strictly equal'}</div>
1111
+ }
1112
+
1113
+ const { body } = await render(App);
1114
+ expect(body).toBeHtml('<div>strict equality</div>');
1115
+ });
1116
+
1117
+ it('handles return with greater than or equal', async () => {
1118
+ component App() {
1119
+ let a = 10;
1120
+ let b = 10;
1121
+
1122
+ if (a >= b) {
1123
+ <div>{'a >= b'}</div>
1124
+ return;
1125
+ }
1126
+ <div>{'a < b'}</div>
1127
+ }
1128
+
1129
+ const { body } = await render(App);
1130
+ expect(body).toBeHtml('<div>a >= b</div>');
1131
+ });
1132
+
1133
+ it('handles return with less than or equal (escaped in HTML)', async () => {
1134
+ component App() {
1135
+ let a = 5;
1136
+ let b = 10;
1137
+
1138
+ if (a <= b) {
1139
+ <div>{'a <= b'}</div>
1140
+ return;
1141
+ }
1142
+ <div>{'a > b'}</div>
1143
+ }
1144
+
1145
+ const { body } = await render(App);
1146
+ expect(body).toBeHtml('<div>a &lt;= b</div>');
1147
+ });
1148
+
1149
+ it('handles return with not equal', async () => {
1150
+ component App() {
1151
+ let a = 5;
1152
+ let b = 10;
1153
+
1154
+ if (a != b) {
1155
+ <div>{'a != b'}</div>
1156
+ return;
1157
+ }
1158
+ <div>{'a == b'}</div>
1159
+ }
1160
+
1161
+ const { body } = await render(App);
1162
+ expect(body).toBeHtml('<div>a != b</div>');
1163
+ });
1164
+
1165
+ it('handles return with strict not equal', async () => {
1166
+ component App() {
1167
+ let a = 5;
1168
+ let b = '5';
1169
+
1170
+ // @ts-expect-error testing strict inequality
1171
+ if (a !== b) {
1172
+ <div>{'a !== b'}</div>
1173
+ return;
1174
+ }
1175
+ <div>{'a === b'}</div>
1176
+ }
1177
+
1178
+ const { body } = await render(App);
1179
+ expect(body).toBeHtml('<div>a !== b</div>');
1180
+ });
1181
+
1182
+ describe('deeply nested conditions with returns', () => {
1183
+ it('handles return inside nested div > if > div > if chain, all false', async () => {
1184
+ component App() {
1185
+ let a = false;
1186
+ let b = false;
1187
+ let c = false;
1188
+ let d = false;
1189
+
1190
+ <div class="outer">
1191
+ if (a) {
1192
+ <span class="a">{'branch a'}</span>
1193
+ }
1194
+ <div class="inner">
1195
+ if (b) {
1196
+ <span class="b">{'branch b'}</span>
1197
+ }
1198
+ if (c) {
1199
+ return;
1200
+ }
1201
+ if (d) {
1202
+ <span class="d">{'branch d'}</span>
1203
+ return;
1204
+ }
1205
+ </div>
1206
+ </div>
1207
+ <div class="after">{'after'}</div>
1208
+ }
1209
+
1210
+ const { body } = await render(App);
1211
+ expect(body).toContain('class="outer"');
1212
+ expect(body).toContain('class="inner"');
1213
+ expect(body).toContain('class="after"');
1214
+ expect(body).not.toContain('class="a"');
1215
+ expect(body).not.toContain('class="b"');
1216
+ expect(body).not.toContain('class="d"');
1217
+ });
1218
+
1219
+ it('nested: first return (c) triggers, hides after', async () => {
1220
+ component App() {
1221
+ let a = false;
1222
+ let b = false;
1223
+ let c = true;
1224
+ let d = false;
1225
+
1226
+ <div class="outer">
1227
+ if (a) {
1228
+ <span class="a">{'branch a'}</span>
1229
+ }
1230
+ <div class="inner">
1231
+ if (b) {
1232
+ <span class="b">{'branch b'}</span>
1233
+ }
1234
+ if (c) {
1235
+ return;
1236
+ }
1237
+ if (d) {
1238
+ <span class="d">{'branch d'}</span>
1239
+ return;
1240
+ }
1241
+ </div>
1242
+ </div>
1243
+ <div class="after">{'after'}</div>
1244
+ }
1245
+
1246
+ const { body } = await render(App);
1247
+ expect(body).toContain('class="outer"');
1248
+ expect(body).toContain('class="inner"');
1249
+ expect(body).not.toContain('class="after"');
1250
+ expect(body).not.toContain('class="d"');
1251
+ });
1252
+
1253
+ it('nested: second return (d) triggers with template, hides after', async () => {
1254
+ component App() {
1255
+ let a = true;
1256
+ let b = true;
1257
+ let c = false;
1258
+ let d = true;
1259
+
1260
+ <div class="outer">
1261
+ if (a) {
1262
+ <span class="a">{'branch a'}</span>
1263
+ }
1264
+ <div class="inner">
1265
+ if (b) {
1266
+ <span class="b">{'branch b'}</span>
1267
+ }
1268
+ if (c) {
1269
+ return;
1270
+ }
1271
+ if (d) {
1272
+ <span class="d">{'branch d'}</span>
1273
+ return;
1274
+ }
1275
+ </div>
1276
+ </div>
1277
+ <div class="after">{'after'}</div>
1278
+ }
1279
+
1280
+ const { body } = await render(App);
1281
+ expect(body).toContain('class="a"');
1282
+ expect(body).toContain('class="b"');
1283
+ expect(body).toContain('class="d"');
1284
+ expect(body).not.toContain('class="after"');
1285
+ });
1286
+
1287
+ it('nested: both returns active, first (c) wins', async () => {
1288
+ component App() {
1289
+ let a = false;
1290
+ let b = false;
1291
+ let c = true;
1292
+ let d = true;
1293
+
1294
+ <div class="outer">
1295
+ if (a) {
1296
+ <span class="a">{'branch a'}</span>
1297
+ }
1298
+ <div class="inner">
1299
+ if (b) {
1300
+ <span class="b">{'branch b'}</span>
1301
+ }
1302
+ if (c) {
1303
+ return;
1304
+ }
1305
+ if (d) {
1306
+ <span class="d">{'branch d'}</span>
1307
+ return;
1308
+ }
1309
+ </div>
1310
+ </div>
1311
+ <div class="after">{'after'}</div>
1312
+ }
1313
+
1314
+ const { body } = await render(App);
1315
+ expect(body).toContain('class="outer"');
1316
+ expect(body).toContain('class="inner"');
1317
+ expect(body).not.toContain('class="d"');
1318
+ expect(body).not.toContain('class="after"');
1319
+ });
1320
+
1321
+ it(
1322
+ 'nested outer/inner return guards render fallback paths when inner guard is false',
1323
+ async () => {
1324
+ component App() {
1325
+ let a = true;
1326
+ let b = false;
1327
+
1328
+ if (a) {
1329
+ <div class="a">{'a'}</div>
1330
+ if (b) {
1331
+ <div class="b">{'b'}</div>
1332
+ return;
1333
+ }
1334
+ <div class="inner-rest">{'inner rest'}</div>
1335
+ }
1336
+
1337
+ <div class="rest">{'rest'}</div>
1338
+ }
1339
+
1340
+ const { body } = await render(App);
1341
+ expect(body).toBeHtml(
1342
+ '<div class="a">a</div><div class="inner-rest">inner rest</div><div class="rest">rest</div>',
1343
+ );
1344
+ },
1345
+ );
1346
+
1347
+ it('emits balanced hydration markers for nested sibling return guards', async () => {
1348
+ component App() {
1349
+ let a = true;
1350
+ let b = false;
1351
+ let c = true;
1352
+
1353
+ if (a) {
1354
+ <div class="a">{'a'}</div>
1355
+ if (b) {
1356
+ <div class="b">{'b'}</div>
1357
+ return;
1358
+ }
1359
+ if (c) {
1360
+ <div class="c">{'c'}</div>
1361
+ return;
1362
+ }
1363
+ <div class="inner-rest">{'inner rest'}</div>
1364
+ }
1365
+
1366
+ <div class="rest">{'rest'}</div>
1367
+ }
1368
+
1369
+ const { body } = await render(App);
1370
+ const open_markers = body.match(/<!--\[-->/g) || [];
1371
+ const close_markers = body.match(/<!--\]-->/g) || [];
1372
+ expect(open_markers.length).toBe(close_markers.length);
1373
+ expect(open_markers.length).toBeGreaterThanOrEqual(3);
1374
+ expect(body).toContain('<div class="a">a</div>');
1375
+ expect(body).toContain('<div class="c">c</div>');
1376
+ expect(body).not.toContain('<div class="rest">rest</div>');
1377
+ });
1378
+ });
1379
+ });