@sola-air-ui/compiler 1.0.8 → 1.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.
- package/package.json +5 -4
- package/src/cli.js +0 -0
- package/src/index.js +1360 -735
- package/types/index.d.ts +45 -0
package/src/index.js
CHANGED
|
@@ -1,735 +1,1360 @@
|
|
|
1
|
-
import * as acorn from 'acorn';
|
|
2
|
-
import * as htmlparser2 from 'htmlparser2';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
|
|
5
|
-
const { version: COMPILER_VERSION } = JSON.parse(
|
|
6
|
-
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
const VOID_ELEMENTS = new Set([
|
|
10
|
-
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
11
|
-
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
12
|
-
]);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
result +=
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
//
|
|
677
|
-
const
|
|
678
|
-
let
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
import * as htmlparser2 from 'htmlparser2';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const { version: COMPILER_VERSION } = JSON.parse(
|
|
6
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const VOID_ELEMENTS = new Set([
|
|
10
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
11
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// Escape a string that is emitted inside a backtick template literal. Without
|
|
15
|
+
// this, a single backtick anywhere in a <style> block (a CSS comment quoting a
|
|
16
|
+
// class name, say) closes the literal early and the rest of the stylesheet is
|
|
17
|
+
// parsed as JavaScript — which fails at runtime, not at compile time. `${` is
|
|
18
|
+
// escaped for the same reason.
|
|
19
|
+
function escapeTemplateLiteral(str) {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/\\/g, '\\\\')
|
|
22
|
+
.replace(/`/g, '\\`')
|
|
23
|
+
.replace(/\$\{/g, '\\${');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hashString(str) {
|
|
27
|
+
let hash = 5381;
|
|
28
|
+
for (let i = 0; i < str.length; i++) {
|
|
29
|
+
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
30
|
+
hash |= 0;
|
|
31
|
+
}
|
|
32
|
+
return 'sola-' + Math.abs(hash).toString(36);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scopeSelectorList(selector, scopeHash) {
|
|
36
|
+
return selector
|
|
37
|
+
.split(',')
|
|
38
|
+
.map(part => {
|
|
39
|
+
const trimmed = part.trim();
|
|
40
|
+
if (!trimmed) return part;
|
|
41
|
+
// Insert scope hash before pseudo-class/element (e.g. button:hover → button.hash:hover)
|
|
42
|
+
const pseudoIdx = trimmed.search(/::?[\w-]/);
|
|
43
|
+
if (pseudoIdx > 0) {
|
|
44
|
+
return `${trimmed.slice(0, pseudoIdx)}.${scopeHash}${trimmed.slice(pseudoIdx)}`;
|
|
45
|
+
}
|
|
46
|
+
return `${trimmed}.${scopeHash}`;
|
|
47
|
+
})
|
|
48
|
+
.join(', ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Scope every rule's selector to this component.
|
|
52
|
+
//
|
|
53
|
+
// Deliberately brace-depth aware rather than a single regex: a regex that treats
|
|
54
|
+
// any run of text before `{` as a selector also rewrites declaration values that
|
|
55
|
+
// happen to contain a brace (`content: "${x}"`, `grid-template-areas`, a data:
|
|
56
|
+
// URI), silently corrupting the stylesheet. Only text at depth 0 — or directly
|
|
57
|
+
// inside an at-rule block such as @media — is a selector.
|
|
58
|
+
function scopeStyles(css, scopeHash) {
|
|
59
|
+
let out = '';
|
|
60
|
+
let buf = ''; // pending selector text
|
|
61
|
+
let depth = 0; // brace nesting
|
|
62
|
+
let inStr = null; // quote char while inside a string
|
|
63
|
+
let inComment = false;
|
|
64
|
+
// Depths that hold declarations rather than nested rules; their contents are
|
|
65
|
+
// copied through untouched.
|
|
66
|
+
const ruleDepths = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < css.length; i++) {
|
|
69
|
+
const ch = css[i];
|
|
70
|
+
const next = css[i + 1];
|
|
71
|
+
|
|
72
|
+
if (inComment) {
|
|
73
|
+
buf += ch;
|
|
74
|
+
if (ch === '*' && next === '/') { buf += next; i++; inComment = false; }
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (inStr) {
|
|
78
|
+
buf += ch;
|
|
79
|
+
if (ch === '\\') { if (next !== undefined) { buf += next; i++; } continue; }
|
|
80
|
+
if (ch === inStr) inStr = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === '/' && next === '*') { buf += ch + next; i++; inComment = true; continue; }
|
|
84
|
+
if (ch === '"' || ch === "'") { inStr = ch; buf += ch; continue; }
|
|
85
|
+
|
|
86
|
+
if (ch === '{') {
|
|
87
|
+
const isDeclarationBlock = depth > 0 && !ruleDepths.includes(depth);
|
|
88
|
+
if (isDeclarationBlock) {
|
|
89
|
+
// A brace inside a declaration value — not a rule. Pass it through.
|
|
90
|
+
buf += ch;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Split the buffer into everything that is not the selector (comments and
|
|
95
|
+
// whitespace) and the selector itself. A comment sitting above a rule —
|
|
96
|
+
// `/* Dark mode */ @media ...` — must not be treated as part of it, or the
|
|
97
|
+
// at-rule is misclassified and the scope hash lands inside the comment.
|
|
98
|
+
const commentEnd = buf.lastIndexOf('*/');
|
|
99
|
+
const selectorStart = commentEnd === -1 ? 0 : commentEnd + 2;
|
|
100
|
+
const afterComments = buf.slice(selectorStart);
|
|
101
|
+
const leadingLen = selectorStart + (afterComments.length - afterComments.trimStart().length);
|
|
102
|
+
const leading = buf.slice(0, leadingLen);
|
|
103
|
+
const selector = buf.slice(leadingLen);
|
|
104
|
+
const trimmed = selector.trim();
|
|
105
|
+
const isAtRule = trimmed.startsWith('@');
|
|
106
|
+
const isKeyframeStop =
|
|
107
|
+
trimmed.startsWith('from') || trimmed.startsWith('to') || /^\d+%/.test(trimmed);
|
|
108
|
+
|
|
109
|
+
// @media/@supports wrap further rules; @keyframes stops and plain rules
|
|
110
|
+
// wrap declarations.
|
|
111
|
+
const wrapsRules = isAtRule && /^@(media|supports|container|layer|scope)\b/.test(trimmed);
|
|
112
|
+
|
|
113
|
+
out += leading;
|
|
114
|
+
out += (isAtRule || isKeyframeStop) ? selector : scopeSelectorList(selector, scopeHash);
|
|
115
|
+
out += ch;
|
|
116
|
+
buf = '';
|
|
117
|
+
depth++;
|
|
118
|
+
if (wrapsRules) ruleDepths.push(depth);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (ch === '}') {
|
|
123
|
+
out += buf + ch;
|
|
124
|
+
buf = '';
|
|
125
|
+
const idx = ruleDepths.indexOf(depth);
|
|
126
|
+
if (idx !== -1) ruleDepths.splice(idx, 1);
|
|
127
|
+
depth = Math.max(0, depth - 1);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
buf += ch;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return out + buf;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function extractScriptAndStyle(source) {
|
|
138
|
+
let script = '';
|
|
139
|
+
let style = '';
|
|
140
|
+
let template = source;
|
|
141
|
+
|
|
142
|
+
let isTypeScript = false;
|
|
143
|
+
// Zero-based line in the original .sola file where the script body starts,
|
|
144
|
+
// so generated output can be mapped back to it.
|
|
145
|
+
let scriptStartLine = 0;
|
|
146
|
+
const scriptMatch = template.match(/<script(?:\s+lang=["'](ts|js)["'])?>([\s\S]*?)<\/script>/);
|
|
147
|
+
if (scriptMatch) {
|
|
148
|
+
isTypeScript = scriptMatch[1] === 'ts';
|
|
149
|
+
script = scriptMatch[2];
|
|
150
|
+
const openTagEnd = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
|
|
151
|
+
scriptStartLine = source.slice(0, openTagEnd).split('\n').length - 1;
|
|
152
|
+
template = template.replace(scriptMatch[0], '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const styleMatch = template.match(/<style(?:\s+scoped)?>([\s\S]*?)<\/style>/);
|
|
156
|
+
if (styleMatch) {
|
|
157
|
+
style = styleMatch[1];
|
|
158
|
+
template = template.replace(styleMatch[0], '');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { script, style, template, isTypeScript, scriptStartLine };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Returns the index of the closing } that matches the { at openPos,
|
|
165
|
+
// respecting string literals and nested braces. Returns -1 if unbalanced.
|
|
166
|
+
function matchBrace(text, openPos) {
|
|
167
|
+
let depth = 0;
|
|
168
|
+
let inStr = null;
|
|
169
|
+
for (let i = openPos; i < text.length; i++) {
|
|
170
|
+
const ch = text[i];
|
|
171
|
+
if (inStr) {
|
|
172
|
+
if (ch === '\\') { i++; continue; }
|
|
173
|
+
if (ch === inStr) inStr = null;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
|
|
177
|
+
if (ch === '{') depth++;
|
|
178
|
+
else if (ch === '}') { if (--depth === 0) return i; }
|
|
179
|
+
}
|
|
180
|
+
return -1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Walk the template and resolve {expr} in attribute values using balanced-brace
|
|
184
|
+
// matching. Both dynamic attributes and event/bind handlers are lifted into side
|
|
185
|
+
// tables and replaced with brace-free markers (__soladyn_N__ / __solahandler_N__).
|
|
186
|
+
//
|
|
187
|
+
// Handlers must be lifted rather than written back as literal attribute text:
|
|
188
|
+
// extractExpressions runs over the whole template afterwards and has no notion
|
|
189
|
+
// of attribute boundaries, so a block-bodied handler like
|
|
190
|
+
// `on:click={() => { n = n() + 1; }}` had its braces eaten and turned into a
|
|
191
|
+
// <sola-expr> tag, which then shredded the rest of the tag into bogus
|
|
192
|
+
// attributes. Markers contain no braces, so nothing downstream can see inside.
|
|
193
|
+
function preprocessTemplate(template, dynAttrs, handlers) {
|
|
194
|
+
const isHandlerAttr = (name) => /^on:?[\w]+$/.test(name) || /^bind:[\w]+$/.test(name);
|
|
195
|
+
let result = '';
|
|
196
|
+
let i = 0;
|
|
197
|
+
while (i < template.length) {
|
|
198
|
+
// Opening tag (not closing, not comment)
|
|
199
|
+
if (template[i] === '<' && template[i + 1] !== '/' && template[i + 1] !== '!') {
|
|
200
|
+
result += '<';
|
|
201
|
+
i++;
|
|
202
|
+
// Tag name
|
|
203
|
+
while (i < template.length && /[^\s>\/]/.test(template[i])) result += template[i++];
|
|
204
|
+
// Attributes
|
|
205
|
+
while (i < template.length && template[i] !== '>' && !(template[i] === '/' && template[i + 1] === '>')) {
|
|
206
|
+
if (/\s/.test(template[i])) { result += template[i++]; continue; }
|
|
207
|
+
// Attribute name
|
|
208
|
+
let attrName = '';
|
|
209
|
+
while (i < template.length && /[^\s=\/>]/.test(template[i])) attrName += template[i++];
|
|
210
|
+
if (!attrName) { result += template[i++]; continue; }
|
|
211
|
+
if (template[i] !== '=') { result += attrName; continue; }
|
|
212
|
+
i++; // skip =
|
|
213
|
+
if (template[i] === '{') {
|
|
214
|
+
// Unquoted brace expression: attr={expr}
|
|
215
|
+
const closeIdx = matchBrace(template, i);
|
|
216
|
+
if (closeIdx !== -1) {
|
|
217
|
+
const expr = template.slice(i + 1, closeIdx).trim();
|
|
218
|
+
if (!expr.startsWith('#') && !expr.startsWith('/') && !expr.startsWith(':')) {
|
|
219
|
+
if (isHandlerAttr(attrName)) {
|
|
220
|
+
handlers.push(expr);
|
|
221
|
+
result += `${attrName}="__solahandler_${handlers.length - 1}__"`;
|
|
222
|
+
} else {
|
|
223
|
+
dynAttrs.push(expr);
|
|
224
|
+
result += `${attrName}="__soladyn_${dynAttrs.length - 1}__"`;
|
|
225
|
+
}
|
|
226
|
+
i = closeIdx + 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
result += `${attrName}={`; i++;
|
|
231
|
+
} else if (template[i] === '"' || template[i] === "'") {
|
|
232
|
+
// Quoted attribute value — scan for {expr} interpolations
|
|
233
|
+
const quote = template[i++];
|
|
234
|
+
const parts = [];
|
|
235
|
+
let currentText = '';
|
|
236
|
+
let hasDyn = false;
|
|
237
|
+
while (i < template.length && template[i] !== quote) {
|
|
238
|
+
if (template[i] === '{') {
|
|
239
|
+
const closeIdx = matchBrace(template, i);
|
|
240
|
+
if (closeIdx !== -1) {
|
|
241
|
+
const expr = template.slice(i + 1, closeIdx).trim();
|
|
242
|
+
if (!expr.startsWith('#') && !expr.startsWith('/') && !expr.startsWith(':')) {
|
|
243
|
+
hasDyn = true;
|
|
244
|
+
parts.push({ text: currentText });
|
|
245
|
+
currentText = '';
|
|
246
|
+
parts.push({ expr });
|
|
247
|
+
i = closeIdx + 1; continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
currentText += template[i++];
|
|
252
|
+
}
|
|
253
|
+
parts.push({ text: currentText });
|
|
254
|
+
if (i < template.length) i++; // skip closing quote
|
|
255
|
+
if (hasDyn && isHandlerAttr(attrName)) {
|
|
256
|
+
// A handler written in the quoted form, e.g. onclick="{() => go()}".
|
|
257
|
+
// Concatenating it as a string would produce an attribute holding
|
|
258
|
+
// source text rather than a wired listener.
|
|
259
|
+
const exprPart = parts.find((p) => p.expr !== undefined);
|
|
260
|
+
handlers.push(exprPart ? exprPart.expr : '() => {}');
|
|
261
|
+
result += `${attrName}="__solahandler_${handlers.length - 1}__"`;
|
|
262
|
+
} else if (hasDyn) {
|
|
263
|
+
// Build a string-concatenation expression instead of a runtime backtick
|
|
264
|
+
// template literal. Confirmed by driving a real browser against a live
|
|
265
|
+
// ServiceNow Service Portal instance: its widget-script delivery pipeline
|
|
266
|
+
// silently mangles `${expr}` interpolation inside a template literal down
|
|
267
|
+
// to bare, unevaluated expression text (e.g. `background: ${x}` becomes
|
|
268
|
+
// the literal string "background: x" — the function reference itself,
|
|
269
|
+
// never called). Every prior "the reactive binding doesn't do anything"
|
|
270
|
+
// report came from this exact code path. Concatenation is semantically
|
|
271
|
+
// identical and isn't affected by whatever that pipeline does to backticks.
|
|
272
|
+
const combined = parts
|
|
273
|
+
.filter((p) => p.expr !== undefined || p.text !== '')
|
|
274
|
+
.map((p) => (p.expr !== undefined ? `(${p.expr})` : JSON.stringify(p.text)))
|
|
275
|
+
.join(' + ');
|
|
276
|
+
dynAttrs.push(combined || `''`);
|
|
277
|
+
result += `${attrName}="__soladyn_${dynAttrs.length - 1}__"`;
|
|
278
|
+
} else {
|
|
279
|
+
result += `${attrName}="${currentText}"`;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
result += `${attrName}=`;
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (i < template.length && template[i] === '/') { result += '/'; i++; }
|
|
287
|
+
if (i < template.length && template[i] === '>') { result += '>'; i++; }
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
result += template[i++];
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Rewrite `{:else if C}` into `{:else}{#if C}` plus a matching `{/if}`, so an
|
|
296
|
+
// else-if chain becomes ordinary nested if/else blocks that the block handling
|
|
297
|
+
// below already understands. Without this the chain is never recognised and
|
|
298
|
+
// every branch's expression is evaluated regardless of the condition — an
|
|
299
|
+
// `{:else if x}{...x.y...}` chain throws on null the moment the first branch
|
|
300
|
+
// is false.
|
|
301
|
+
function desugarElseIf(template) {
|
|
302
|
+
const OPEN = '{#if';
|
|
303
|
+
const CLOSE = '{/if}';
|
|
304
|
+
|
|
305
|
+
// Index of the {/if} matching the {#if} that starts at openIdx.
|
|
306
|
+
function matchingClose(text, openIdx) {
|
|
307
|
+
let depth = 0;
|
|
308
|
+
let i = openIdx;
|
|
309
|
+
while (i < text.length) {
|
|
310
|
+
if (text.startsWith(OPEN, i)) { depth++; i += OPEN.length; continue; }
|
|
311
|
+
if (text.startsWith(CLOSE, i)) {
|
|
312
|
+
depth--;
|
|
313
|
+
if (depth === 0) return i;
|
|
314
|
+
i += CLOSE.length;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
i++;
|
|
318
|
+
}
|
|
319
|
+
return -1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// First `{:else if ...}` that belongs to this block rather than a nested one.
|
|
323
|
+
function findElseIf(text, from, to) {
|
|
324
|
+
let depth = 0;
|
|
325
|
+
let i = from;
|
|
326
|
+
while (i < to) {
|
|
327
|
+
if (text.startsWith(OPEN, i)) { depth++; i += OPEN.length; continue; }
|
|
328
|
+
if (text.startsWith(CLOSE, i)) { depth--; i += CLOSE.length; continue; }
|
|
329
|
+
if (depth === 0 && text.startsWith('{:else', i)) {
|
|
330
|
+
const end = text.indexOf('}', i);
|
|
331
|
+
if (end === -1) return null;
|
|
332
|
+
const inner = text.slice(i + '{:else'.length, end);
|
|
333
|
+
if (/^\s+if\s+/.test(inner)) {
|
|
334
|
+
return { start: i, end, condition: inner.replace(/^\s+if\s+/, '').trim() };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
i++;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let result = template;
|
|
343
|
+
let searchFrom = 0;
|
|
344
|
+
while (true) {
|
|
345
|
+
const openIdx = result.indexOf(OPEN, searchFrom);
|
|
346
|
+
if (openIdx === -1) break;
|
|
347
|
+
|
|
348
|
+
const closeIdx = matchingClose(result, openIdx);
|
|
349
|
+
if (closeIdx === -1) { searchFrom = openIdx + OPEN.length; continue; }
|
|
350
|
+
|
|
351
|
+
const hit = findElseIf(result, openIdx + OPEN.length, closeIdx);
|
|
352
|
+
if (!hit) { searchFrom = openIdx + OPEN.length; continue; }
|
|
353
|
+
|
|
354
|
+
// {:else if C} → {:else}{#if C} ... {/if}, the extra close going just
|
|
355
|
+
// before this block's own {/if}.
|
|
356
|
+
result =
|
|
357
|
+
result.slice(0, hit.start) +
|
|
358
|
+
'{:else}{#if ' + hit.condition + '}' +
|
|
359
|
+
result.slice(hit.end + 1, closeIdx) +
|
|
360
|
+
CLOSE +
|
|
361
|
+
result.slice(closeIdx);
|
|
362
|
+
// Re-examine this same block: a chain may hold several else-ifs.
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Convert {#if}/{:else}/{/if} and {#each}/{/each} blocks into the sola-* tag
|
|
369
|
+
// form the DOM emitter understands.
|
|
370
|
+
//
|
|
371
|
+
// Written as a recursive scanner rather than the regexes this replaced: a
|
|
372
|
+
// non-greedy regex cannot count nesting, so `{#if a}…{:else}{#if b}…{/if}{/if}`
|
|
373
|
+
// captured an unbalanced fragment as the else branch and produced structurally
|
|
374
|
+
// wrong output. Anything deeper than two levels was silently corrupted.
|
|
375
|
+
function transformBlocks(template) {
|
|
376
|
+
const attr = (s) => String(s).trim().replace(/&/g, '&').replace(/"/g, '"');
|
|
377
|
+
|
|
378
|
+
// Index of the tag closing the block opened at `openIdx`, honouring nesting.
|
|
379
|
+
function matchEnd(text, openIdx, openTag, closeTag) {
|
|
380
|
+
let depth = 0;
|
|
381
|
+
let i = openIdx;
|
|
382
|
+
while (i < text.length) {
|
|
383
|
+
if (text.startsWith(openTag, i)) { depth++; i += openTag.length; continue; }
|
|
384
|
+
if (text.startsWith(closeTag, i)) {
|
|
385
|
+
depth--;
|
|
386
|
+
if (depth === 0) return i;
|
|
387
|
+
i += closeTag.length;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
i++;
|
|
391
|
+
}
|
|
392
|
+
return -1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Position of this block's own {:else}, ignoring any belonging to nested blocks.
|
|
396
|
+
function topLevelElse(text, from, to) {
|
|
397
|
+
let depth = 0;
|
|
398
|
+
let i = from;
|
|
399
|
+
while (i < to) {
|
|
400
|
+
if (text.startsWith('{#if', i)) { depth++; i += 4; continue; }
|
|
401
|
+
if (text.startsWith('{/if}', i)) { depth--; i += 5; continue; }
|
|
402
|
+
if (depth === 0 && text.startsWith('{:else}', i)) return i;
|
|
403
|
+
i++;
|
|
404
|
+
}
|
|
405
|
+
return -1;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function walk(text) {
|
|
409
|
+
let out = '';
|
|
410
|
+
let i = 0;
|
|
411
|
+
while (i < text.length) {
|
|
412
|
+
if (text.startsWith('{#if', i)) {
|
|
413
|
+
const condEnd = text.indexOf('}', i);
|
|
414
|
+
if (condEnd === -1) { out += text[i++]; continue; }
|
|
415
|
+
const condition = text.slice(i + 4, condEnd);
|
|
416
|
+
const end = matchEnd(text, i, '{#if', '{/if}');
|
|
417
|
+
if (end === -1) { out += text[i++]; continue; }
|
|
418
|
+
|
|
419
|
+
const body = text.slice(condEnd + 1, end);
|
|
420
|
+
const elseIdx = topLevelElse(body, 0, body.length);
|
|
421
|
+
const thenPart = elseIdx === -1 ? body : body.slice(0, elseIdx);
|
|
422
|
+
const elsePart = elseIdx === -1 ? null : body.slice(elseIdx + '{:else}'.length);
|
|
423
|
+
|
|
424
|
+
out += `<sola-if condition="${attr(condition)}"><sola-then>${walk(thenPart)}</sola-then>`;
|
|
425
|
+
if (elsePart !== null) out += `<sola-else>${walk(elsePart)}</sola-else>`;
|
|
426
|
+
out += `</sola-if>`;
|
|
427
|
+
i = end + '{/if}'.length;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (text.startsWith('{#each', i)) {
|
|
432
|
+
const headEnd = text.indexOf('}', i);
|
|
433
|
+
if (headEnd === -1) { out += text[i++]; continue; }
|
|
434
|
+
const head = text.slice(i, headEnd + 1);
|
|
435
|
+
const parsed = /^{#each\s+([\s\S]*?)\s+as\s+([\w$]+)(?:\s*,\s*([\w$]+))?(?:\s*\(([^)]+)\))?}$/.exec(head);
|
|
436
|
+
const end = matchEnd(text, i, '{#each', '{/each}');
|
|
437
|
+
if (!parsed || end === -1) { out += text[i++]; continue; }
|
|
438
|
+
|
|
439
|
+
const [, arr, item, index, key] = parsed;
|
|
440
|
+
out += `<sola-each array="${attr(arr)}" item="${attr(item)}"`;
|
|
441
|
+
if (index) out += ` index="${attr(index)}"`;
|
|
442
|
+
if (key) out += ` key="${attr(key)}"`;
|
|
443
|
+
out += `>${walk(text.slice(headEnd + 1, end))}</sola-each>`;
|
|
444
|
+
i = end + '{/each}'.length;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
out += text[i++];
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return walk(template);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Replace {expr} in text content with <sola-expr> tags, using balanced-brace
|
|
457
|
+
// matching so expressions like {fn({key: val})} work correctly.
|
|
458
|
+
function extractExpressions(text) {
|
|
459
|
+
let result = '';
|
|
460
|
+
let i = 0;
|
|
461
|
+
while (i < text.length) {
|
|
462
|
+
if (text[i] === '{') {
|
|
463
|
+
const closeIdx = matchBrace(text, i);
|
|
464
|
+
if (closeIdx === -1) { result += text[i++]; continue; }
|
|
465
|
+
const inner = text.slice(i + 1, closeIdx).trim();
|
|
466
|
+
if (inner.startsWith('#') || inner.startsWith('/') || inner.startsWith(':')) {
|
|
467
|
+
result += text.slice(i, closeIdx + 1); i = closeIdx + 1;
|
|
468
|
+
} else if (inner.startsWith('@html')) {
|
|
469
|
+
const htmlExpr = inner.slice('@html'.length).trim();
|
|
470
|
+
result += `<sola-html expr="${htmlExpr.replace(/"/g, '"')}"></sola-html>`;
|
|
471
|
+
i = closeIdx + 1;
|
|
472
|
+
} else {
|
|
473
|
+
result += `<sola-expr expr="${inner.replace(/"/g, '"')}"></sola-expr>`;
|
|
474
|
+
i = closeIdx + 1;
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
result += text[i++];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Strip TypeScript type annotations so Acorn (plain JS parser) can handle
|
|
484
|
+
// script blocks written in TS. Covers the common patterns: type annotations,
|
|
485
|
+
// interface/type declarations, angle-bracket generics, and `as` casts.
|
|
486
|
+
function stripTypeScript(code) {
|
|
487
|
+
// Remove interface and type alias declarations
|
|
488
|
+
code = code.replace(/^\s*(export\s+)?(interface|type)\s+\w[\s\S]*?(?=\n(?:export|const|let|var|function|class|\/\/|$))/gm, '');
|
|
489
|
+
// Remove inline type annotations: `: Type` before = , ) , ; , , or end of line
|
|
490
|
+
code = code.replace(/:\s*[\w<>\[\]|&{},\s.?]+(?=[=,);{}\n])/g, '');
|
|
491
|
+
// Remove `as Type` casts
|
|
492
|
+
code = code.replace(/\s+as\s+[\w<>\[\]|&{}.?]+/g, '');
|
|
493
|
+
// Remove generic type parameters from function/class declarations
|
|
494
|
+
code = code.replace(/<[^>()=]+>(?=\s*\()/g, '');
|
|
495
|
+
return code;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─── Reactive auto-call transform ───
|
|
499
|
+
// Makes the parenthesis-free syntax real: bare reads of $state/$derived vars
|
|
500
|
+
// compile to getter calls (count → count(), user.name → user().name), so
|
|
501
|
+
// `$derived(count * 2)` and `{doubled}` behave the way the docs show them.
|
|
502
|
+
// Explicit calls (count()) are left alone, keeping the README syntax valid.
|
|
503
|
+
|
|
504
|
+
function collectPatternNames(node, out) {
|
|
505
|
+
if (!node) return;
|
|
506
|
+
switch (node.type) {
|
|
507
|
+
case 'Identifier': out.add(node.name); break;
|
|
508
|
+
case 'ObjectPattern': node.properties.forEach(p => collectPatternNames(p.value || p.argument, out)); break;
|
|
509
|
+
case 'ArrayPattern': node.elements.forEach(el => collectPatternNames(el, out)); break;
|
|
510
|
+
case 'AssignmentPattern': collectPatternNames(node.left, out); break;
|
|
511
|
+
case 'RestElement': collectPatternNames(node.argument, out); break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Collect every name declared anywhere inside a function body. Deliberately
|
|
516
|
+
// conservative: a name declared in ANY nested scope suppresses auto-call for
|
|
517
|
+
// that name within the function, preferring a missed rewrite over a wrong one.
|
|
518
|
+
function collectDeclaredNames(node, out) {
|
|
519
|
+
if (!node || typeof node !== 'object' || !node.type) return;
|
|
520
|
+
if (node.type === 'VariableDeclaration') node.declarations.forEach(d => collectPatternNames(d.id, out));
|
|
521
|
+
if ((node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') && node.id) out.add(node.id.name);
|
|
522
|
+
if (node.type === 'CatchClause' && node.param) collectPatternNames(node.param, out);
|
|
523
|
+
for (const key of Object.keys(node)) {
|
|
524
|
+
if (key === 'type') continue;
|
|
525
|
+
const child = node[key];
|
|
526
|
+
if (Array.isArray(child)) child.forEach(c => collectDeclaredNames(c, out));
|
|
527
|
+
else if (child && typeof child === 'object' && child.type) collectDeclaredNames(child, out);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Pre-pass: find every $state/$derived/$intent/$data declaration up front so
|
|
532
|
+
// reads that appear earlier in the source (hoisted functions) still rewrite.
|
|
533
|
+
function collectReactiveVars(ast) {
|
|
534
|
+
const vars = { state: new Set(), derived: new Set(), intent: new Set(), data: new Set() };
|
|
535
|
+
(function walk(node) {
|
|
536
|
+
if (!node || typeof node !== 'object' || !node.type) return;
|
|
537
|
+
if (node.type === 'VariableDeclaration') {
|
|
538
|
+
for (const decl of node.declarations) {
|
|
539
|
+
if (decl.init && decl.init.type === 'CallExpression' &&
|
|
540
|
+
decl.init.callee.type === 'Identifier' && decl.id.type === 'Identifier') {
|
|
541
|
+
const macro = decl.init.callee.name;
|
|
542
|
+
if (macro === '$state' || macro === '$persist') vars.state.add(decl.id.name);
|
|
543
|
+
else if (macro === '$derived') vars.derived.add(decl.id.name);
|
|
544
|
+
else if (macro === '$intent') vars.intent.add(decl.id.name);
|
|
545
|
+
else if (macro === '$data') vars.data.add(decl.id.name);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (const key of Object.keys(node)) {
|
|
550
|
+
if (key === 'type') continue;
|
|
551
|
+
const child = node[key];
|
|
552
|
+
if (Array.isArray(child)) child.forEach(walk);
|
|
553
|
+
else if (child && typeof child === 'object' && child.type) walk(child);
|
|
554
|
+
}
|
|
555
|
+
})(ast);
|
|
556
|
+
return vars;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const DATA_ACCESSOR_METHODS = new Set(['refetch', 'stop']);
|
|
560
|
+
const INTENT_SUB_SIGNALS = new Set(['loading', 'error']);
|
|
561
|
+
|
|
562
|
+
function reactiveKind(name, vars, shadowStack) {
|
|
563
|
+
for (const scope of shadowStack) if (scope.has(name)) return null;
|
|
564
|
+
if (vars.state.has(name)) return 'state';
|
|
565
|
+
if (vars.derived.has(name)) return 'derived';
|
|
566
|
+
if (vars.intent.has(name)) return 'intent';
|
|
567
|
+
if (vars.data.has(name)) return 'data';
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Walk an AST emitting position edits that implement the reactive rewrite:
|
|
572
|
+
// - `count = v` → `set_count(v)` (bracket edits so children still walk)
|
|
573
|
+
// - `count += v` → `set_count(count() + (v))`
|
|
574
|
+
// - `count++` → `set_count(count() + 1)`
|
|
575
|
+
// - bare read → `count()`; member-object read `user.name` → `user().name`
|
|
576
|
+
// - `{count}` shorthand in object literals → `{count: count()}`
|
|
577
|
+
// - $intent accessors: `x.loading` → `x.loading()` (sub-signals live on the fn)
|
|
578
|
+
// - $data accessors: `x.data` → `x().data`, but `x.refetch`/`x.stop` untouched
|
|
579
|
+
function emitReactiveEdits(rootNode, src, vars, edits, initialShadows) {
|
|
580
|
+
function walk(node, parent, shadows) {
|
|
581
|
+
if (!node || typeof node !== 'object' || !node.type) return;
|
|
582
|
+
|
|
583
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
584
|
+
const scope = new Set();
|
|
585
|
+
if (node.id) scope.add(node.id.name);
|
|
586
|
+
(node.params || []).forEach(p => collectPatternNames(p, scope));
|
|
587
|
+
collectDeclaredNames(node.body, scope);
|
|
588
|
+
shadows = [...shadows, scope];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' &&
|
|
592
|
+
reactiveKind(node.left.name, vars, shadows) === 'state') {
|
|
593
|
+
const name = node.left.name;
|
|
594
|
+
if (node.operator === '=') {
|
|
595
|
+
edits.push({ start: node.start, end: node.right.start, replacement: `set_${name}(` });
|
|
596
|
+
edits.push({ start: node.end, end: node.end, replacement: `)` });
|
|
597
|
+
} else {
|
|
598
|
+
const rawOp = node.operator.slice(0, -1);
|
|
599
|
+
edits.push({ start: node.start, end: node.right.start, replacement: `set_${name}(${name}() ${rawOp} (` });
|
|
600
|
+
edits.push({ start: node.end, end: node.end, replacement: `))` });
|
|
601
|
+
}
|
|
602
|
+
walk(node.right, node, shadows);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (node.type === 'UpdateExpression' && node.argument.type === 'Identifier' &&
|
|
607
|
+
reactiveKind(node.argument.name, vars, shadows) === 'state') {
|
|
608
|
+
const name = node.argument.name;
|
|
609
|
+
const op = node.operator === '++' ? '+' : '-';
|
|
610
|
+
edits.push({ start: node.start, end: node.end, replacement: `set_${name}(${name}() ${op} 1)` });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// $intent sub-signals: x.loading / x.error are functions hung on the accessor
|
|
615
|
+
if (node.type === 'MemberExpression' && !node.computed &&
|
|
616
|
+
node.object.type === 'Identifier' && node.property.type === 'Identifier' &&
|
|
617
|
+
reactiveKind(node.object.name, vars, shadows) === 'intent' &&
|
|
618
|
+
INTENT_SUB_SIGNALS.has(node.property.name) &&
|
|
619
|
+
!(parent && parent.type === 'CallExpression' && parent.callee === node)) {
|
|
620
|
+
edits.push({ start: node.end, end: node.end, replacement: `()` });
|
|
621
|
+
return; // children need no further rewriting
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (node.type === 'Identifier') {
|
|
625
|
+
const kind = reactiveKind(node.name, vars, shadows);
|
|
626
|
+
if (kind) {
|
|
627
|
+
// A fragment root (e.g. the whole expression is just `count`) has no
|
|
628
|
+
// parent — treat it as a plain read position.
|
|
629
|
+
const p = parent || { type: '__fragment_root__' };
|
|
630
|
+
const isDeclOrBinding =
|
|
631
|
+
(p.type === 'VariableDeclarator' && p.id === node) ||
|
|
632
|
+
((p.type === 'FunctionDeclaration' || p.type === 'FunctionExpression' || p.type === 'ClassDeclaration') && p.id === node) ||
|
|
633
|
+
(p.type === 'AssignmentPattern' && p.left === node) ||
|
|
634
|
+
p.type === 'RestElement' ||
|
|
635
|
+
p.type === 'ImportSpecifier' || p.type === 'ImportDefaultSpecifier' ||
|
|
636
|
+
p.type === 'ExportSpecifier' || p.type === 'LabeledStatement' ||
|
|
637
|
+
p.type === 'BreakStatement' || p.type === 'ContinueStatement';
|
|
638
|
+
const isMemberProp = p.type === 'MemberExpression' && p.property === node && !p.computed;
|
|
639
|
+
const isCallee = (p.type === 'CallExpression' || p.type === 'NewExpression') && p.callee === node;
|
|
640
|
+
const isAssignTarget = (p.type === 'AssignmentExpression' && p.left === node) || p.type === 'UpdateExpression';
|
|
641
|
+
const isPropertyKey = p.type === 'Property' && p.key === node && !p.computed && !p.shorthand;
|
|
642
|
+
const isShorthand = p.type === 'Property' && p.shorthand && p.value === node;
|
|
643
|
+
const isMemberObject = p.type === 'MemberExpression' && p.object === node;
|
|
644
|
+
|
|
645
|
+
if (!isDeclOrBinding && !isMemberProp && !isCallee && !isAssignTarget && !isPropertyKey) {
|
|
646
|
+
if (isShorthand) {
|
|
647
|
+
edits.push({ start: node.start, end: node.end, replacement: `${node.name}: ${node.name}()` });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (isMemberObject) {
|
|
651
|
+
if (kind === 'state' || kind === 'derived' ||
|
|
652
|
+
(kind === 'data' && !(p.property.type === 'Identifier' && !p.computed && DATA_ACCESSOR_METHODS.has(p.property.name)))) {
|
|
653
|
+
edits.push({ start: node.end, end: node.end, replacement: `()` });
|
|
654
|
+
}
|
|
655
|
+
// intent accessors keep their function identity for member access
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
edits.push({ start: node.end, end: node.end, replacement: `()` });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (const key of Object.keys(node)) {
|
|
665
|
+
if (key === 'type') continue;
|
|
666
|
+
const child = node[key];
|
|
667
|
+
if (Array.isArray(child)) child.forEach(c => walk(c, node, shadows));
|
|
668
|
+
else if (child && typeof child === 'object' && child.type) walk(child, node, shadows);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
walk(rootNode, null, initialShadows || []);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function applyEdits(src, edits) {
|
|
675
|
+
// Descending by start (then end) so earlier positions stay valid; zero-width
|
|
676
|
+
// inserts at a shared position land before same-position range replacements.
|
|
677
|
+
const sorted = [...edits].sort((a, b) => (b.start - a.start) || (b.end - a.end));
|
|
678
|
+
let out = src;
|
|
679
|
+
for (const edit of sorted) {
|
|
680
|
+
out = out.slice(0, edit.start) + edit.replacement + out.slice(edit.end);
|
|
681
|
+
}
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const NO_REACTIVE_VARS = { state: new Set(), derived: new Set(), intent: new Set(), data: new Set() };
|
|
686
|
+
|
|
687
|
+
function hasReactiveVars(vars) {
|
|
688
|
+
return vars.state.size > 0 || vars.derived.size > 0 || vars.intent.size > 0 || vars.data.size > 0;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Rewrite one template expression (text interpolation, dynamic attribute,
|
|
692
|
+
// if-condition, each-source, event handler) with the reactive auto-call rules.
|
|
693
|
+
// `locals` holds template-scope names (each-item, each-index) that shadow signals.
|
|
694
|
+
// Unparseable input is returned unchanged — the emitter will surface it as-is.
|
|
695
|
+
function transformTemplateExpression(src, vars, locals) {
|
|
696
|
+
if (!hasReactiveVars(vars) || !src || !src.trim()) return src;
|
|
697
|
+
let ast;
|
|
698
|
+
try {
|
|
699
|
+
ast = acorn.parseExpressionAt(src, 0, { ecmaVersion: 'latest' });
|
|
700
|
+
} catch {
|
|
701
|
+
return src;
|
|
702
|
+
}
|
|
703
|
+
const edits = [];
|
|
704
|
+
emitReactiveEdits(ast, src, vars, edits, locals && locals.size ? [locals] : []);
|
|
705
|
+
return edits.length > 0 ? applyEdits(src, edits) : src;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ─── Source maps ───
|
|
709
|
+
// The generated module inlines the user's script verbatim (indented by two
|
|
710
|
+
// spaces) into the mount function, so every line of that block maps 1:1 back to
|
|
711
|
+
// its line in the .sola file. That covers the case that matters: a runtime error
|
|
712
|
+
// or breakpoint inside user code pointing at user code rather than at generated
|
|
713
|
+
// output. Emitted DOM instructions have no user-authored counterpart and are
|
|
714
|
+
// deliberately left unmapped.
|
|
715
|
+
|
|
716
|
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
717
|
+
|
|
718
|
+
function encodeVlq(value) {
|
|
719
|
+
let vlq = value < 0 ? ((-value) << 1) | 1 : value << 1;
|
|
720
|
+
let out = '';
|
|
721
|
+
do {
|
|
722
|
+
let digit = vlq & 0b11111;
|
|
723
|
+
vlq >>>= 5;
|
|
724
|
+
if (vlq > 0) digit |= 0b100000;
|
|
725
|
+
out += B64[digit];
|
|
726
|
+
} while (vlq > 0);
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Build a v3 source map from a list of {generatedLine, sourceLine} pairs
|
|
732
|
+
* (both zero-based), each mapping column 0 to column 0 of the source line.
|
|
733
|
+
*/
|
|
734
|
+
function buildSourceMap(segments, filename, sourceContent, generatedLineCount) {
|
|
735
|
+
const byLine = new Map();
|
|
736
|
+
for (const seg of segments) byLine.set(seg.generatedLine, seg.sourceLine);
|
|
737
|
+
|
|
738
|
+
let prevSourceLine = 0;
|
|
739
|
+
const lines = [];
|
|
740
|
+
for (let line = 0; line < generatedLineCount; line++) {
|
|
741
|
+
if (!byLine.has(line)) { lines.push(''); continue; }
|
|
742
|
+
const sourceLine = byLine.get(line);
|
|
743
|
+
// [generatedColumn, sourceIndex, sourceLine, sourceColumn], all relative
|
|
744
|
+
// except generatedColumn which resets each line.
|
|
745
|
+
lines.push(
|
|
746
|
+
encodeVlq(0) + encodeVlq(0) + encodeVlq(sourceLine - prevSourceLine) + encodeVlq(0)
|
|
747
|
+
);
|
|
748
|
+
prevSourceLine = sourceLine;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
version: 3,
|
|
753
|
+
file: filename ? filename.replace(/\.sola$/, '.js') : undefined,
|
|
754
|
+
sources: [filename || 'component.sola'],
|
|
755
|
+
sourcesContent: [sourceContent],
|
|
756
|
+
names: [],
|
|
757
|
+
mappings: lines.join(';')
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export function compile(source, options = {}) {
|
|
762
|
+
const target = options.target || 'esm'; // 'esm' | 'iife'
|
|
763
|
+
const { script: scriptContent, style: styleContent, template: rawTemplateSource, isTypeScript, scriptStartLine } = extractScriptAndStyle(source);
|
|
764
|
+
const scopeHash = hashString(styleContent || rawTemplateSource);
|
|
765
|
+
const hasStyles = styleContent.trim().length > 0;
|
|
766
|
+
const scopedCss = hasStyles ? scopeStyles(styleContent, scopeHash) : '';
|
|
767
|
+
|
|
768
|
+
// Flatten else-if chains into nested if/else before anything else looks at
|
|
769
|
+
// the template, so the block handling below only ever sees the simple form.
|
|
770
|
+
let rawTemplate = desugarElseIf(rawTemplateSource);
|
|
771
|
+
|
|
772
|
+
// Pre-process attribute-bound expressions BEFORE extractExpressions using
|
|
773
|
+
// balanced-brace matching (handles nested object literals, ternaries, etc.)
|
|
774
|
+
const dynAttrs = [];
|
|
775
|
+
const handlers = [];
|
|
776
|
+
rawTemplate = preprocessTemplate(rawTemplate, dynAttrs, handlers);
|
|
777
|
+
|
|
778
|
+
// Resolve a __solahandler_N__ marker back to its expression. Anything else is
|
|
779
|
+
// passed through, so a handler written as a bare identifier still works.
|
|
780
|
+
const resolveHandler = (val) => {
|
|
781
|
+
const m = /^__solahandler_(\d+)__$/.exec(val);
|
|
782
|
+
return m ? handlers[parseInt(m[1], 10)] : val;
|
|
783
|
+
};
|
|
784
|
+
rawTemplate = extractExpressions(rawTemplate);
|
|
785
|
+
|
|
786
|
+
// Convert {#if}/{:else}/{/if} and {#each}/{/each} into the sola-* tag form
|
|
787
|
+
// the emitter walks.
|
|
788
|
+
rawTemplate = transformBlocks(rawTemplate);
|
|
789
|
+
|
|
790
|
+
// Parse HTML AST
|
|
791
|
+
const templateNodes = [];
|
|
792
|
+
let root = { type: 'element', name: '__root__', attribs: {}, children: [] };
|
|
793
|
+
templateNodes.push(root);
|
|
794
|
+
|
|
795
|
+
const parser = new htmlparser2.Parser({
|
|
796
|
+
onopentag(name, attribs) {
|
|
797
|
+
const node = { type: 'element', name, attribs, children: [] };
|
|
798
|
+
templateNodes[templateNodes.length - 1].children.push(node);
|
|
799
|
+
if (!VOID_ELEMENTS.has(name)) {
|
|
800
|
+
templateNodes.push(node);
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
ontext(text) {
|
|
804
|
+
if (text.trim().length > 0 && templateNodes.length > 0) {
|
|
805
|
+
templateNodes[templateNodes.length - 1].children.push({ type: 'text', data: text });
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
onclosetag(name) {
|
|
809
|
+
if (templateNodes.length > 1 && !VOID_ELEMENTS.has(name)) {
|
|
810
|
+
templateNodes.pop();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}, { recognizeSelfClosing: true, lowerCaseTags: false, lowerCaseAttributeNames: false });
|
|
814
|
+
|
|
815
|
+
parser.write(rawTemplate.trim() || '<div></div>');
|
|
816
|
+
parser.end();
|
|
817
|
+
|
|
818
|
+
// Parse JS AST with Acorn
|
|
819
|
+
// Strip TS first — AST positions and all slice() calls operate on the stripped source.
|
|
820
|
+
// Only run on <script lang="ts"> — the strip regex can't reliably distinguish a type
|
|
821
|
+
// annotation's `:` from a ternary's `: falsyBranch` or an object literal's `key: value`,
|
|
822
|
+
// so applying it to plain JS risks corrupting valid code. Scripts that don't opt into TS
|
|
823
|
+
// are left untouched.
|
|
824
|
+
const strippedScript = scriptContent.trim() && isTypeScript ? stripTypeScript(scriptContent) : scriptContent;
|
|
825
|
+
let jsOutput = strippedScript;
|
|
826
|
+
const stateVars = new Set();
|
|
827
|
+
const exportedProps = [];
|
|
828
|
+
const componentImports = [];
|
|
829
|
+
|
|
830
|
+
let reactiveVars = NO_REACTIVE_VARS;
|
|
831
|
+
if (strippedScript.trim()) {
|
|
832
|
+
try {
|
|
833
|
+
const ast = acorn.parse(strippedScript, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
834
|
+
const edits = [];
|
|
835
|
+
// Whole-node removals (imports, exported props): any rewrite edit that
|
|
836
|
+
// falls inside one of these spans must be dropped, not applied on top.
|
|
837
|
+
const removedRanges = [];
|
|
838
|
+
|
|
839
|
+
// Pre-pass so reads that appear before their $state declaration
|
|
840
|
+
// (hoisted handler functions) still rewrite correctly.
|
|
841
|
+
reactiveVars = collectReactiveVars(ast);
|
|
842
|
+
for (const name of reactiveVars.state) stateVars.add(name);
|
|
843
|
+
|
|
844
|
+
function walkAST(node, parent) {
|
|
845
|
+
if (!node || typeof node !== 'object') return;
|
|
846
|
+
|
|
847
|
+
// Track component imports
|
|
848
|
+
if (node.type === 'ImportDeclaration') {
|
|
849
|
+
const src = node.source.value;
|
|
850
|
+
if (src.endsWith('.sola')) {
|
|
851
|
+
node.specifiers.forEach(spec => {
|
|
852
|
+
componentImports.push({ localName: spec.local.name, path: src });
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
edits.push({
|
|
856
|
+
start: node.start,
|
|
857
|
+
end: node.end,
|
|
858
|
+
replacement: `// import hoisted: ${node.source.value}`
|
|
859
|
+
});
|
|
860
|
+
removedRanges.push([node.start, node.end]);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Exported props
|
|
864
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration &&
|
|
865
|
+
node.declaration.type === 'VariableDeclaration') {
|
|
866
|
+
node.declaration.declarations.forEach(decl => {
|
|
867
|
+
const name = decl.id.name;
|
|
868
|
+
const defaultVal = decl.init
|
|
869
|
+
? strippedScript.slice(decl.init.start, decl.init.end)
|
|
870
|
+
: 'undefined';
|
|
871
|
+
exportedProps.push({ name, default: defaultVal });
|
|
872
|
+
});
|
|
873
|
+
edits.push({ start: node.start, end: node.end, replacement: '' });
|
|
874
|
+
removedRanges.push([node.start, node.end]);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// $state()/$derived()/$effect()/$intent()/$data() macro declarations.
|
|
878
|
+
// Bracket edits (head + tail) leave the argument expression in place so
|
|
879
|
+
// the reactive rewrite pass below can still auto-call signals inside it.
|
|
880
|
+
if (node.type === 'VariableDeclaration' && node.declarations.length === 1) {
|
|
881
|
+
const decl = node.declarations[0];
|
|
882
|
+
if (decl.init && decl.init.type === 'CallExpression' && decl.init.callee.type === 'Identifier') {
|
|
883
|
+
const macro = decl.init.callee.name;
|
|
884
|
+
const arg = decl.init.arguments[0];
|
|
885
|
+
const lastArg = decl.init.arguments[decl.init.arguments.length - 1];
|
|
886
|
+
if (macro === '$state' || macro === '$persist') {
|
|
887
|
+
// $persist(key, initial, options) is $state backed by storage and
|
|
888
|
+
// synced across tabs/pages; it reads and assigns exactly like $state.
|
|
889
|
+
const varName = decl.id.name;
|
|
890
|
+
const factory = macro === '$persist' ? 'createPersistentSignal' : 'createSignal';
|
|
891
|
+
if (arg) {
|
|
892
|
+
edits.push({ start: node.start, end: arg.start, replacement: `const [${varName}, set_${varName}] = ${factory}(` });
|
|
893
|
+
edits.push({ start: lastArg.end, end: node.end, replacement: `);` });
|
|
894
|
+
} else {
|
|
895
|
+
edits.push({ start: node.start, end: node.end, replacement: `const [${varName}, set_${varName}] = createSignal(undefined);` });
|
|
896
|
+
}
|
|
897
|
+
} else if (macro === '$derived') {
|
|
898
|
+
const varName = decl.id.name;
|
|
899
|
+
if (arg) {
|
|
900
|
+
const isFn = arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression';
|
|
901
|
+
edits.push({
|
|
902
|
+
start: node.start,
|
|
903
|
+
end: arg.start,
|
|
904
|
+
replacement: isFn ? `const ${varName} = createDerived(` : `const ${varName} = createDerived(() => `
|
|
905
|
+
});
|
|
906
|
+
edits.push({ start: arg.end, end: node.end, replacement: `);` });
|
|
907
|
+
} else {
|
|
908
|
+
edits.push({ start: node.start, end: node.end, replacement: `const ${varName} = createDerived(() => undefined);` });
|
|
909
|
+
}
|
|
910
|
+
} else if (macro === '$effect') {
|
|
911
|
+
if (arg) {
|
|
912
|
+
edits.push({ start: node.start, end: arg.start, replacement: `createEffect(` });
|
|
913
|
+
edits.push({ start: arg.end, end: node.end, replacement: `);` });
|
|
914
|
+
} else {
|
|
915
|
+
edits.push({ start: node.start, end: node.end, replacement: `createEffect(() => {});` });
|
|
916
|
+
}
|
|
917
|
+
} else if (macro === '$intent') {
|
|
918
|
+
if (arg) {
|
|
919
|
+
edits.push({ start: node.start, end: arg.start, replacement: `const ${decl.id.name} = createIntent(` });
|
|
920
|
+
edits.push({ start: lastArg.end, end: node.end, replacement: `);` });
|
|
921
|
+
} else {
|
|
922
|
+
edits.push({ start: node.start, end: node.end, replacement: `const ${decl.id.name} = createIntent('');` });
|
|
923
|
+
}
|
|
924
|
+
} else if (macro === '$data') {
|
|
925
|
+
if (arg) {
|
|
926
|
+
edits.push({ start: node.start, end: arg.start, replacement: `const ${decl.id.name} = createData(` });
|
|
927
|
+
edits.push({ start: lastArg.end, end: node.end, replacement: `);` });
|
|
928
|
+
} else {
|
|
929
|
+
edits.push({ start: node.start, end: node.end, replacement: `const ${decl.id.name} = createData('');` });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
for (const key of Object.keys(node)) {
|
|
936
|
+
if (key === 'type') continue;
|
|
937
|
+
const child = node[key];
|
|
938
|
+
if (Array.isArray(child)) {
|
|
939
|
+
child.forEach(c => walkAST(c, node));
|
|
940
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
941
|
+
walkAST(child, node);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
walkAST(ast, null);
|
|
947
|
+
|
|
948
|
+
// Reactive rewrite pass: assignments/updates to signals become setter
|
|
949
|
+
// calls, bare signal reads become getter calls. Edits landing inside a
|
|
950
|
+
// removed import/export span are dropped.
|
|
951
|
+
const rewriteEdits = [];
|
|
952
|
+
emitReactiveEdits(ast, strippedScript, reactiveVars, rewriteEdits, []);
|
|
953
|
+
for (const edit of rewriteEdits) {
|
|
954
|
+
if (removedRanges.some(([s, e]) => edit.start >= s && edit.end <= e)) continue;
|
|
955
|
+
edits.push(edit);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
jsOutput = applyEdits(strippedScript, edits);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
const loc = err.loc ? ` (line ${err.loc.line}, col ${err.loc.column})` : '';
|
|
961
|
+
const filePath = typeof options === 'string' ? options : (options.filename || '<script>');
|
|
962
|
+
throw new Error(`[sola compiler] Parse error in ${filePath}${loc}:\n ${err.message}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Generate DOM creation instructions
|
|
967
|
+
let uid = 0;
|
|
968
|
+
let domCode = '';
|
|
969
|
+
const importedComponentSet = new Set(componentImports.map(i => i.localName));
|
|
970
|
+
|
|
971
|
+
const sourceFile = options.filename || '<component>';
|
|
972
|
+
|
|
973
|
+
// Reject a template expression that is not valid JavaScript, instead of
|
|
974
|
+
// emitting it and letting the bundler report a syntax error inside generated
|
|
975
|
+
// code the author never wrote. The usual cause is a literal brace in markup —
|
|
976
|
+
// a code sample containing `setInterval(() => {` — where `{` opens an
|
|
977
|
+
// expression, so everything up to the next `}` is swallowed.
|
|
978
|
+
function checkExpression(expr, whatFor) {
|
|
979
|
+
try {
|
|
980
|
+
const trimmed = expr.trim();
|
|
981
|
+
const node = acorn.parseExpressionAt(trimmed, 0, { ecmaVersion: 'latest' });
|
|
982
|
+
// parseExpressionAt stops at the first complete expression and ignores
|
|
983
|
+
// whatever follows, so `{<span>…}` only fails once the whole string has
|
|
984
|
+
// to be consumed.
|
|
985
|
+
if (node.end !== trimmed.length) throw new Error('trailing content');
|
|
986
|
+
} catch {
|
|
987
|
+
const preview = expr.length > 80 ? expr.slice(0, 80) + '…' : expr;
|
|
988
|
+
throw new Error(
|
|
989
|
+
`[sola compiler] Invalid ${whatFor} in ${sourceFile}:\n` +
|
|
990
|
+
` {${preview}}\n` +
|
|
991
|
+
` This is not a valid JavaScript expression. If you meant a literal ` +
|
|
992
|
+
`brace in your markup, escape it as { and }.`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function emitNode(node, parentVar, locals = new Set()) {
|
|
998
|
+
if (!node) return;
|
|
999
|
+
// Template-expression rewrite with the current template scope (each-item /
|
|
1000
|
+
// each-index names shadow signals of the same name).
|
|
1001
|
+
const rewrite = (expr) => transformTemplateExpression(expr, reactiveVars, locals);
|
|
1002
|
+
|
|
1003
|
+
if (node.type === 'text') {
|
|
1004
|
+
const text = node.data.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
|
1005
|
+
if (text.trim().length > 0) {
|
|
1006
|
+
const id = `t${uid++}`;
|
|
1007
|
+
domCode += ` const ${id} = document.createTextNode(\`${text}\`);\n`;
|
|
1008
|
+
domCode += ` ${parentVar}.appendChild(${id});\n`;
|
|
1009
|
+
}
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (node.name === 'sola-expr') {
|
|
1014
|
+
const raw = (node.attribs.expr || '').replace(/"/g, '"');
|
|
1015
|
+
checkExpression(raw, 'template expression');
|
|
1016
|
+
const expr = rewrite(raw);
|
|
1017
|
+
const id = `e${uid++}`;
|
|
1018
|
+
domCode += ` const ${id} = document.createTextNode('');\n`;
|
|
1019
|
+
domCode += ` ${parentVar}.appendChild(${id});\n`;
|
|
1020
|
+
// Parenthesize expr before `?? ''`: JS forbids mixing ?? with || or && at the
|
|
1021
|
+
// same level without explicit grouping (e.g. `col.label || col ?? ''` is a
|
|
1022
|
+
// SyntaxError), and any {a || b}-style fallback expression — an extremely
|
|
1023
|
+
// common pattern — would otherwise produce invalid generated code.
|
|
1024
|
+
domCode += ` createEffect(() => { ${id}.textContent = String((${expr}) ?? ''); });\n`;
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// {@html expr} — renders expr as raw HTML via innerHTML, unlike {expr} which always
|
|
1029
|
+
// goes through textContent. Wrapped in its own element so setting innerHTML can't
|
|
1030
|
+
// clobber unrelated sibling nodes. Callers are responsible for trusting/sanitizing
|
|
1031
|
+
// whatever markup they pass — same contract as innerHTML anywhere else.
|
|
1032
|
+
if (node.name === 'sola-html') {
|
|
1033
|
+
const expr = rewrite((node.attribs.expr || '').replace(/"/g, '"'));
|
|
1034
|
+
const id = `h${uid++}`;
|
|
1035
|
+
domCode += ` const ${id} = document.createElement('div');\n`;
|
|
1036
|
+
domCode += ` ${id}.style.display = 'contents';\n`;
|
|
1037
|
+
domCode += ` ${parentVar}.appendChild(${id});\n`;
|
|
1038
|
+
domCode += ` createEffect(() => { ${id}.innerHTML = String((${expr}) ?? ''); });\n`;
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Nested custom component
|
|
1043
|
+
if (importedComponentSet.has(node.name)) {
|
|
1044
|
+
const cid = `comp${uid++}`;
|
|
1045
|
+
domCode += ` const ${cid}_target = document.createElement('div');\n`;
|
|
1046
|
+
domCode += ` ${cid}_target.className = 'sola-component-root';\n`;
|
|
1047
|
+
domCode += ` ${parentVar}.appendChild(${cid}_target);\n`;
|
|
1048
|
+
|
|
1049
|
+
// Dynamically-bound props (rows={items}) arrive here as __soladyn_N__ placeholders —
|
|
1050
|
+
// resolve them back to the real expression from dynAttrs and pass it as a raw
|
|
1051
|
+
// (unquoted) property so the child receives live data, not the placeholder text.
|
|
1052
|
+
//
|
|
1053
|
+
// `onXxx`-named props (e.g. onChange={handler}) arrive as a
|
|
1054
|
+
// __solahandler_N__ marker, because preprocessTemplate cannot tell a
|
|
1055
|
+
// component apart from a native element and lifts both the same way.
|
|
1056
|
+
// Either way the expression is passed through as a live function.
|
|
1057
|
+
const propEntries = [];
|
|
1058
|
+
for (const [key, val] of Object.entries(node.attribs || {})) {
|
|
1059
|
+
const dynMatch = /^__soladyn_(\d+)__$/.exec(val);
|
|
1060
|
+
const handlerMatch = /^__solahandler_(\d+)__$/.exec(val);
|
|
1061
|
+
// Dynamic (non-event) component props are deliberately NOT auto-called:
|
|
1062
|
+
// passing the getter is how a child receives live data today, and
|
|
1063
|
+
// README-syntax components rely on it.
|
|
1064
|
+
const propValue = dynMatch
|
|
1065
|
+
? `(${dynAttrs[parseInt(dynMatch[1], 10)]})`
|
|
1066
|
+
: handlerMatch
|
|
1067
|
+
? `(${rewrite(handlers[parseInt(handlerMatch[1], 10)])})`
|
|
1068
|
+
: JSON.stringify(val);
|
|
1069
|
+
propEntries.push(`${JSON.stringify(key)}: ${propValue}`);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Slot content: compile the tag's children into a function the child component can
|
|
1073
|
+
// call to project them wherever it renders a <slot> in its own template.
|
|
1074
|
+
if (node.children && node.children.length > 0) {
|
|
1075
|
+
const slotVar = `${cid}_slot`;
|
|
1076
|
+
domCode += ` const ${slotVar} = (__slot_target) => {\n`;
|
|
1077
|
+
node.children.forEach(child => emitNode(child, '__slot_target', locals));
|
|
1078
|
+
domCode += ` };\n`;
|
|
1079
|
+
propEntries.push(`children: ${slotVar}`);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
domCode += ` ${node.name}(${cid}_target, { ${propEntries.join(', ')} });\n`;
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Slot placeholder: projects the children passed by a parent component, if any.
|
|
1087
|
+
if (node.name === 'slot') {
|
|
1088
|
+
domCode += ` if (typeof props.children === 'function') { props.children(${parentVar}); }\n`;
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// {#if ...} {:else} ... {/if}
|
|
1093
|
+
if (node.name === 'sola-if') {
|
|
1094
|
+
const cid = `c${uid++}`;
|
|
1095
|
+
domCode += ` const ${cid}_a = document.createComment('if');\n`;
|
|
1096
|
+
domCode += ` ${parentVar}.appendChild(${cid}_a);\n`;
|
|
1097
|
+
domCode += ` let ${cid}_els = [];\n`;
|
|
1098
|
+
|
|
1099
|
+
const thenChild = node.children.find(c => c.name === 'sola-then');
|
|
1100
|
+
const elseChild = node.children.find(c => c.name === 'sola-else');
|
|
1101
|
+
|
|
1102
|
+
domCode += ` createEffect(() => {\n`;
|
|
1103
|
+
domCode += ` ${cid}_els.forEach(e => e.remove()); ${cid}_els = [];\n`;
|
|
1104
|
+
domCode += ` if (${rewrite(node.attribs.condition)}) {\n`;
|
|
1105
|
+
if (thenChild) {
|
|
1106
|
+
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1107
|
+
thenChild.children.forEach(child => emitNode(child, 'f', locals));
|
|
1108
|
+
domCode += ` Array.from(f.childNodes).forEach(n => ${cid}_els.push(n));\n`;
|
|
1109
|
+
domCode += ` ${cid}_a.parentNode.insertBefore(f, ${cid}_a.nextSibling);\n`;
|
|
1110
|
+
}
|
|
1111
|
+
domCode += ` } else {\n`;
|
|
1112
|
+
if (elseChild) {
|
|
1113
|
+
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1114
|
+
elseChild.children.forEach(child => emitNode(child, 'f', locals));
|
|
1115
|
+
domCode += ` Array.from(f.childNodes).forEach(n => ${cid}_els.push(n));\n`;
|
|
1116
|
+
domCode += ` ${cid}_a.parentNode.insertBefore(f, ${cid}_a.nextSibling);\n`;
|
|
1117
|
+
}
|
|
1118
|
+
domCode += ` }\n`;
|
|
1119
|
+
domCode += ` });\n`;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// {#each} with optional keyed diffing: {#each items as item (item.id)}
|
|
1124
|
+
if (node.name === 'sola-each') {
|
|
1125
|
+
const eid = `e${uid++}`;
|
|
1126
|
+
// Item/index names shadow signals inside the loop body and key expression
|
|
1127
|
+
const eachLocals = new Set(locals);
|
|
1128
|
+
eachLocals.add(node.attribs.item);
|
|
1129
|
+
if (node.attribs.index) eachLocals.add(node.attribs.index);
|
|
1130
|
+
const arrayExpr = rewrite(node.attribs.array);
|
|
1131
|
+
const keyExpr = node.attribs.key
|
|
1132
|
+
? transformTemplateExpression(node.attribs.key, reactiveVars, eachLocals)
|
|
1133
|
+
: null;
|
|
1134
|
+
domCode += ` const ${eid}_a = document.createComment('each');\n`;
|
|
1135
|
+
domCode += ` ${parentVar}.appendChild(${eid}_a);\n`;
|
|
1136
|
+
if (keyExpr) {
|
|
1137
|
+
// Keyed: maintain a Map<key, Node[]> and reconcile on each run
|
|
1138
|
+
// Reconcile by walking the new order and moving each key's nodes into
|
|
1139
|
+
// place against a cursor. The previous version only ever inserted, and
|
|
1140
|
+
// against an anchor it never advanced — so reordering a keyed list (a
|
|
1141
|
+
// sort, a drag, a reverse) left the DOM in its original order, silently.
|
|
1142
|
+
domCode += ` let ${eid}_keyMap = new Map();
|
|
1143
|
+
`;
|
|
1144
|
+
domCode += ` createEffect(() => {
|
|
1145
|
+
`;
|
|
1146
|
+
domCode += ` const _items = ${arrayExpr} || [];
|
|
1147
|
+
`;
|
|
1148
|
+
domCode += ` const _parent = ${eid}_a.parentNode;
|
|
1149
|
+
`;
|
|
1150
|
+
domCode += ` const _seen = new Set();
|
|
1151
|
+
`;
|
|
1152
|
+
domCode += ` let _cursor = ${eid}_a.nextSibling;
|
|
1153
|
+
`;
|
|
1154
|
+
domCode += ` for (let _i = 0; _i < _items.length; _i++) {
|
|
1155
|
+
`;
|
|
1156
|
+
domCode += ` const ${node.attribs.item} = _items[_i];
|
|
1157
|
+
`;
|
|
1158
|
+
if (node.attribs.index) domCode += ` const ${node.attribs.index} = _i;
|
|
1159
|
+
`;
|
|
1160
|
+
domCode += ` const _key = String(${keyExpr});
|
|
1161
|
+
`;
|
|
1162
|
+
domCode += ` _seen.add(_key);
|
|
1163
|
+
`;
|
|
1164
|
+
domCode += ` let _nodes = ${eid}_keyMap.get(_key);
|
|
1165
|
+
`;
|
|
1166
|
+
domCode += ` if (!_nodes) {
|
|
1167
|
+
`;
|
|
1168
|
+
domCode += ` const f = document.createDocumentFragment();
|
|
1169
|
+
`;
|
|
1170
|
+
node.children.forEach(child => emitNode(child, 'f', eachLocals));
|
|
1171
|
+
domCode += ` _nodes = Array.from(f.childNodes);
|
|
1172
|
+
`;
|
|
1173
|
+
domCode += ` ${eid}_keyMap.set(_key, _nodes);
|
|
1174
|
+
`;
|
|
1175
|
+
domCode += ` }
|
|
1176
|
+
`;
|
|
1177
|
+
domCode += ` for (const _n of _nodes) {
|
|
1178
|
+
`;
|
|
1179
|
+
domCode += ` if (_n === _cursor) { _cursor = _cursor.nextSibling; }
|
|
1180
|
+
`;
|
|
1181
|
+
domCode += ` else { _parent.insertBefore(_n, _cursor); }
|
|
1182
|
+
`;
|
|
1183
|
+
domCode += ` }
|
|
1184
|
+
`;
|
|
1185
|
+
domCode += ` }
|
|
1186
|
+
`;
|
|
1187
|
+
domCode += ` for (const [_k, _stale] of ${eid}_keyMap) {
|
|
1188
|
+
`;
|
|
1189
|
+
domCode += ` if (!_seen.has(_k)) { _stale.forEach(n => n.remove()); ${eid}_keyMap.delete(_k); }
|
|
1190
|
+
`;
|
|
1191
|
+
domCode += ` }
|
|
1192
|
+
`;
|
|
1193
|
+
domCode += ` });
|
|
1194
|
+
`;
|
|
1195
|
+
} else {
|
|
1196
|
+
// Unkeyed: full teardown + rebuild
|
|
1197
|
+
domCode += ` let ${eid}_els = [];\n`;
|
|
1198
|
+
domCode += ` createEffect(() => {\n`;
|
|
1199
|
+
domCode += ` ${eid}_els.forEach(e => e.remove()); ${eid}_els = [];\n`;
|
|
1200
|
+
domCode += ` const _items = ${arrayExpr} || [];\n`;
|
|
1201
|
+
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1202
|
+
domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
|
|
1203
|
+
domCode += ` const ${node.attribs.item} = _items[_i];\n`;
|
|
1204
|
+
if (node.attribs.index) domCode += ` const ${node.attribs.index} = _i;\n`;
|
|
1205
|
+
node.children.forEach(child => emitNode(child, 'f', eachLocals));
|
|
1206
|
+
domCode += ` }\n`;
|
|
1207
|
+
domCode += ` Array.from(f.childNodes).forEach(n => ${eid}_els.push(n));\n`;
|
|
1208
|
+
domCode += ` ${eid}_a.parentNode.insertBefore(f, ${eid}_a.nextSibling);\n`;
|
|
1209
|
+
domCode += ` });\n`;
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Normal element
|
|
1215
|
+
const id = `n${uid++}`;
|
|
1216
|
+
domCode += ` const ${id} = document.createElement('${node.name}');\n`;
|
|
1217
|
+
|
|
1218
|
+
if (hasStyles) {
|
|
1219
|
+
domCode += ` ${id}.classList.add('${scopeHash}');\n`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const BOOLEAN_ATTRS = new Set(['disabled', 'checked', 'selected', 'readonly', 'required', 'multiple', 'open']);
|
|
1223
|
+
for (const [key, val] of Object.entries(node.attribs || {})) {
|
|
1224
|
+
// Dynamic attribute pre-processed from {expr} in attribute values
|
|
1225
|
+
const dynMatch = /^__soladyn_(\d+)__$/.exec(val);
|
|
1226
|
+
if (dynMatch) {
|
|
1227
|
+
const expr = rewrite(dynAttrs[parseInt(dynMatch[1])]);
|
|
1228
|
+
if (BOOLEAN_ATTRS.has(key)) {
|
|
1229
|
+
domCode += ` createEffect(() => { const _v = !!(${expr}); if (_v) ${id}.setAttribute('${key}', ''); else ${id}.removeAttribute('${key}'); });\n`;
|
|
1230
|
+
} else if (key === 'class' || key === 'className') {
|
|
1231
|
+
const suffix = hasStyles ? ` + ' ${scopeHash}'` : '';
|
|
1232
|
+
domCode += ` createEffect(() => { ${id}.className = (${expr})${suffix}; });\n`;
|
|
1233
|
+
} else {
|
|
1234
|
+
domCode += ` createEffect(() => { ${id}.setAttribute('${key}', String(${expr})); });\n`;
|
|
1235
|
+
}
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (key.startsWith('on:')) {
|
|
1239
|
+
const eventName = key.slice(3);
|
|
1240
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(eventName)) continue;
|
|
1241
|
+
domCode += ` ${id}.addEventListener('${eventName}', ${rewrite(resolveHandler(val))});\n`;
|
|
1242
|
+
} else if (key.startsWith('on')) {
|
|
1243
|
+
const eventName = key.slice(2).toLowerCase();
|
|
1244
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(eventName)) continue;
|
|
1245
|
+
domCode += ` ${id}.addEventListener('${eventName}', ${rewrite(resolveHandler(val))});\n`;
|
|
1246
|
+
} else if (key.startsWith('bind:')) {
|
|
1247
|
+
const prop = key.slice(5);
|
|
1248
|
+
// bind:value={name} names a signal; the marker resolves back to it.
|
|
1249
|
+
const bound = resolveHandler(val);
|
|
1250
|
+
if (prop === 'value') {
|
|
1251
|
+
domCode += ` ${id}.addEventListener('input', (e) => { set_${bound}(e.target.value); });\n`;
|
|
1252
|
+
domCode += ` createEffect(() => { ${id}.value = ${bound}() ?? ''; });\n`;
|
|
1253
|
+
}
|
|
1254
|
+
} else if (key === 'class' || key === 'className') {
|
|
1255
|
+
// Static values: emit as a plain string literal. A template literal here
|
|
1256
|
+
// would let a backtick or ${ in the markup break out into expression
|
|
1257
|
+
// position; JSON.stringify cannot.
|
|
1258
|
+
domCode += ` ${id}.className += ' ' + ${JSON.stringify(val)};\n`;
|
|
1259
|
+
} else {
|
|
1260
|
+
domCode += ` ${id}.setAttribute(${JSON.stringify(key)}, ${JSON.stringify(val)});\n`;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
domCode += ` ${parentVar}.appendChild(${id});\n`;
|
|
1265
|
+
|
|
1266
|
+
if (node.children && !VOID_ELEMENTS.has(node.name)) {
|
|
1267
|
+
for (const child of node.children) {
|
|
1268
|
+
emitNode(child, id, locals);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
root.children.forEach(child => emitNode(child, '__target'));
|
|
1274
|
+
|
|
1275
|
+
// Assemble final output module
|
|
1276
|
+
const CORE_EXPORTS = 'createSignal, createDerived, createEffect, createIntent, createData, createPersistentSignal, onMount, onDestroy, pushContext, popContext, __flush_mounts, __flush_destroys';
|
|
1277
|
+
let output = '';
|
|
1278
|
+
output += `// Compiled by @sola-air-ui/compiler v${COMPILER_VERSION}\n`;
|
|
1279
|
+
|
|
1280
|
+
if (target === 'iife') {
|
|
1281
|
+
// ServiceNow / no-bundler mode: pull from window.SolaCore global
|
|
1282
|
+
output += `(function() {\n`;
|
|
1283
|
+
output += `const { ${CORE_EXPORTS} } = window.SolaCore;\n`;
|
|
1284
|
+
for (const imp of componentImports) {
|
|
1285
|
+
output += `// Note: sub-component ${imp.localName} must be pre-loaded before this script\n`;
|
|
1286
|
+
}
|
|
1287
|
+
output += `\n`;
|
|
1288
|
+
output += `window['${options.exportName || 'SolaComponent'}'] = function mount(__target, props = {}) {\n`;
|
|
1289
|
+
} else {
|
|
1290
|
+
output += `import { ${CORE_EXPORTS} } from '@sola-air-ui/core';\n`;
|
|
1291
|
+
for (const imp of componentImports) {
|
|
1292
|
+
output += `import ${imp.localName} from '${imp.path}';\n`;
|
|
1293
|
+
}
|
|
1294
|
+
output += `\n`;
|
|
1295
|
+
output += `export default function mount(__target, props = {}) {\n`;
|
|
1296
|
+
}
|
|
1297
|
+
output += ` const __ctx = pushContext();\n\n`;
|
|
1298
|
+
|
|
1299
|
+
if (hasStyles) {
|
|
1300
|
+
// Styles go into whichever root the component actually lives in: the
|
|
1301
|
+
// ShadowRoot when mounted inside one (document.head styles can't cross a
|
|
1302
|
+
// shadow boundary), otherwise document.head. De-duplicated per root.
|
|
1303
|
+
output += ` if (typeof document !== 'undefined') {\n`;
|
|
1304
|
+
output += ` const __rootNode = __target && __target.getRootNode ? __target.getRootNode() : document;\n`;
|
|
1305
|
+
output += ` const __styleHost = (typeof ShadowRoot !== 'undefined' && __rootNode instanceof ShadowRoot) ? __rootNode : document.head;\n`;
|
|
1306
|
+
output += ` const __seen = __styleHost.__solaStyles || (__styleHost.__solaStyles = new Set());\n`;
|
|
1307
|
+
output += ` if (!__seen.has('${scopeHash}')) {\n`;
|
|
1308
|
+
output += ` __seen.add('${scopeHash}');\n`;
|
|
1309
|
+
output += ` const styleEl = document.createElement('style');\n`;
|
|
1310
|
+
output += ` styleEl.textContent = \`${escapeTemplateLiteral(scopedCss)}\`;\n`;
|
|
1311
|
+
output += ` __styleHost.appendChild(styleEl);\n`;
|
|
1312
|
+
output += ` }\n`;
|
|
1313
|
+
output += ` }\n\n`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
for (const prop of exportedProps) {
|
|
1317
|
+
output += ` let ${prop.name} = props.${prop.name} !== undefined ? props.${prop.name} : ${prop.default};\n`;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
output += `\n // User script\n`;
|
|
1321
|
+
// Record where the user's script lands in the output so each of its lines can
|
|
1322
|
+
// be mapped back to the same line of the .sola file.
|
|
1323
|
+
const userScriptGeneratedStart = output.split('\n').length - 1;
|
|
1324
|
+
const userScriptLines = jsOutput.split('\n');
|
|
1325
|
+
output += userScriptLines.map(l => ' ' + l).join('\n') + '\n\n';
|
|
1326
|
+
|
|
1327
|
+
output += ` // Reactive DOM graph\n`;
|
|
1328
|
+
output += domCode;
|
|
1329
|
+
|
|
1330
|
+
output += `\n __flush_mounts(__ctx);\n`;
|
|
1331
|
+
output += `\n return () => {\n`;
|
|
1332
|
+
output += ` __flush_destroys(__ctx);\n`;
|
|
1333
|
+
output += ` popContext(__ctx);\n`;
|
|
1334
|
+
output += ` __target.innerHTML = '';\n`;
|
|
1335
|
+
output += ` };\n`;
|
|
1336
|
+
output += `};\n`;
|
|
1337
|
+
if (target === 'iife') {
|
|
1338
|
+
output += `})();\n`;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// The reactive rewrite can add or remove characters within a line but never
|
|
1342
|
+
// splits one, so the user script's line count is preserved and each line maps
|
|
1343
|
+
// straight back to its origin.
|
|
1344
|
+
let map = null;
|
|
1345
|
+
if (options.sourcemap !== false) {
|
|
1346
|
+
const filename = options.filename || 'component.sola';
|
|
1347
|
+
const segments = userScriptLines.map((_, i) => ({
|
|
1348
|
+
generatedLine: userScriptGeneratedStart + i,
|
|
1349
|
+
sourceLine: scriptStartLine + i
|
|
1350
|
+
}));
|
|
1351
|
+
map = buildSourceMap(segments, filename, source, output.split('\n').length);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return {
|
|
1355
|
+
code: output,
|
|
1356
|
+
scopeHash,
|
|
1357
|
+
css: scopedCss,
|
|
1358
|
+
map
|
|
1359
|
+
};
|
|
1360
|
+
}
|