vibe-coding-master 0.8.13 → 0.9.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/README.md +7 -3
- package/dist/backend/cli/install-vcm-harness.js +57 -18
- package/dist/backend/gateway/gateway-service.js +5 -3
- package/dist/backend/runtime/terminal-submit.js +1 -0
- package/dist/backend/server.js +14 -4
- package/dist/backend/services/app-settings-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +4 -3
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +2 -13
- package/dist/backend/services/claude-version-check.js +46 -0
- package/dist/backend/services/gate-review-service.js +47 -8
- package/dist/backend/services/harness-service.js +41 -20
- package/dist/backend/services/message-service.js +30 -6
- package/dist/backend/services/project-service.js +8 -2
- package/dist/backend/services/runtime-recovery-service.js +36 -9
- package/dist/backend/services/session-service.js +10 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-process-exit-service.js +4 -1
- package/dist/backend/services/workflow-control-service.js +55 -11
- package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
- package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
- package/dist/backend/templates/harness/gate-review.js +676 -643
- package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
- package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
- package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
- package/dist/shared/types/app-settings.js +31 -0
- package/dist-frontend/assets/index-CShYYQQa.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.js} +41 -41
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +332 -298
- package/scripts/harness-tools/generate-module-index +1047 -512
- package/scripts/harness-tools/generate-public-surface +1231 -1069
- package/scripts/harness-tools/package.json +3 -0
- package/scripts/harness-tools/run-long-check +608 -531
- package/scripts/harness-tools/vcm-artifact +181 -113
- package/scripts/harness-tools/vcm-bash-guard +839 -605
- package/scripts/harness-tools/vcm-subagent-guard +67 -63
- package/scripts/harness-tools/watch-job +313 -238
- package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
|
@@ -1,1077 +1,1239 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
TS_EXPORT_DECLARATION =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
if
|
|
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
|
-
depth
|
|
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
|
-
|
|
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
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
while cursor <
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
depth_bracket += 1
|
|
741
|
-
elif char == "]" and depth_bracket:
|
|
742
|
-
depth_bracket -= 1
|
|
743
|
-
|
|
744
|
-
at_top_level = depth_angle == 0 and depth_paren == 0 and depth_bracket == 0
|
|
745
|
-
if at_top_level and char in "{;":
|
|
746
|
-
return " ".join(source[start:cursor].split())
|
|
747
|
-
cursor += 1
|
|
748
|
-
|
|
749
|
-
return " ".join(source[start:].split())
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
def ts_item_kind(match: re.Match) -> tuple[str, str] | None:
|
|
753
|
-
for kind in ("function", "class", "interface", "type", "enum", "variable"):
|
|
754
|
-
name = match.group(kind)
|
|
755
|
-
if name:
|
|
756
|
-
return ("const" if kind == "variable" else kind, name)
|
|
757
|
-
return None
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
def extract_ts_exported_declarations(source: str, source_file: str) -> list[dict]:
|
|
761
|
-
sanitized = remove_ts_comments_preserve_literals(source)
|
|
762
|
-
items: list[dict] = []
|
|
763
|
-
|
|
764
|
-
for match in TS_EXPORT_DECLARATION.finditer(sanitized):
|
|
765
|
-
kind_and_name = ts_item_kind(match)
|
|
766
|
-
if kind_and_name is None:
|
|
767
|
-
continue
|
|
768
|
-
kind, name = kind_and_name
|
|
769
|
-
items.append(
|
|
770
|
-
{
|
|
771
|
-
"path": name,
|
|
772
|
-
"kind": kind,
|
|
773
|
-
"name": name,
|
|
774
|
-
"source": {
|
|
775
|
-
"path": source_file,
|
|
776
|
-
"line": line_number_from_offset(sanitized, match.start()),
|
|
777
|
-
},
|
|
778
|
-
"signature": ts_signature_from_source(source, match.start()),
|
|
779
|
-
}
|
|
780
|
-
)
|
|
781
|
-
|
|
782
|
-
return items
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
def exported_names_from_group(group: str) -> list[tuple[str, str]]:
|
|
786
|
-
names: list[tuple[str, str]] = []
|
|
787
|
-
body = group.strip()[1:-1]
|
|
788
|
-
for raw_part in body.split(","):
|
|
789
|
-
part = raw_part.strip()
|
|
790
|
-
if not part:
|
|
791
|
-
continue
|
|
792
|
-
if part.startswith("type "):
|
|
793
|
-
part = part.removeprefix("type ").strip()
|
|
794
|
-
pieces = [piece.strip() for piece in re.split(r"\s+as\s+", part, maxsplit=1)]
|
|
795
|
-
source_name = pieces[0]
|
|
796
|
-
public_name = pieces[1] if len(pieces) == 2 else source_name
|
|
797
|
-
if source_name and source_name != "default":
|
|
798
|
-
names.append((source_name, public_name))
|
|
799
|
-
return names
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
def resolve_ts_specifier(current_file: Path, specifier: str, indexed_sources: dict[Path, str]) -> str | None:
|
|
803
|
-
if not specifier.startswith("."):
|
|
804
|
-
return None
|
|
805
|
-
|
|
806
|
-
base = (current_file.parent / specifier).resolve()
|
|
807
|
-
candidates: list[Path] = []
|
|
808
|
-
|
|
809
|
-
if base.suffix:
|
|
810
|
-
candidates.append(base)
|
|
811
|
-
for extension in TS_SOURCE_EXTENSIONS:
|
|
812
|
-
candidates.append(base.with_suffix(extension))
|
|
813
|
-
else:
|
|
814
|
-
for extension in TS_SOURCE_EXTENSIONS:
|
|
815
|
-
candidates.append(base.with_suffix(extension))
|
|
816
|
-
for extension in TS_SOURCE_EXTENSIONS:
|
|
817
|
-
candidates.append(base / f"index{extension}")
|
|
818
|
-
|
|
819
|
-
for candidate in candidates:
|
|
820
|
-
source_file = indexed_sources.get(candidate.resolve())
|
|
821
|
-
if source_file is not None:
|
|
822
|
-
return source_file
|
|
823
|
-
|
|
824
|
-
return None
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
def package_export_targets(value) -> list[str]:
|
|
828
|
-
if isinstance(value, str):
|
|
829
|
-
return [value]
|
|
830
|
-
if isinstance(value, list):
|
|
831
|
-
targets: list[str] = []
|
|
832
|
-
for item in value:
|
|
833
|
-
targets.extend(package_export_targets(item))
|
|
834
|
-
return targets
|
|
835
|
-
if isinstance(value, dict):
|
|
836
|
-
targets: list[str] = []
|
|
837
|
-
for key, item in value.items():
|
|
838
|
-
if key in {"types", "import", "require", "default", "module"} or key.startswith("."):
|
|
839
|
-
targets.extend(package_export_targets(item))
|
|
840
|
-
return targets
|
|
841
|
-
return []
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
def ts_entrypoint_files(root: Path, module: dict) -> list[str]:
|
|
845
|
-
indexed_sources = set(module.get("files", {}).get("source", []))
|
|
846
|
-
module_path = module.get("path", ".")
|
|
847
|
-
module_root = root if module_path == "." else root / module_path
|
|
848
|
-
manifest = load_optional_json(root / module.get("manifest", ""))
|
|
849
|
-
targets: list[str] = []
|
|
850
|
-
|
|
851
|
-
for key in ("exports", "main", "module", "types"):
|
|
852
|
-
targets.extend(package_export_targets(manifest.get(key)))
|
|
853
|
-
|
|
854
|
-
candidates: list[str] = []
|
|
855
|
-
for target in targets:
|
|
856
|
-
if not isinstance(target, str):
|
|
857
|
-
continue
|
|
858
|
-
target_path = (module_root / target).resolve()
|
|
859
|
-
for candidate in [target_path, *[target_path.with_suffix(ext) for ext in TS_SOURCE_EXTENSIONS]]:
|
|
860
|
-
rel = relative_path(candidate, root)
|
|
861
|
-
if rel in indexed_sources:
|
|
862
|
-
candidates.append(rel)
|
|
863
|
-
|
|
864
|
-
if not candidates:
|
|
865
|
-
for name in ("index", "main", "app"):
|
|
866
|
-
for extension in TS_SOURCE_EXTENSIONS:
|
|
867
|
-
rel = relative_path(module_root / "src" / f"{name}{extension}", root)
|
|
868
|
-
if rel in indexed_sources:
|
|
869
|
-
candidates.append(rel)
|
|
870
|
-
|
|
871
|
-
if not candidates and module_path.startswith("apps/"):
|
|
872
|
-
candidates.extend(module.get("files", {}).get("source", []))
|
|
873
|
-
|
|
874
|
-
return sorted(dict.fromkeys(candidates))
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
def collect_ts_public_items(
|
|
878
|
-
root: Path,
|
|
879
|
-
source_file: str,
|
|
880
|
-
module: dict,
|
|
881
|
-
direct_definitions: dict[str, dict[str, dict]],
|
|
882
|
-
indexed_sources: dict[Path, str],
|
|
883
|
-
visited: set[str],
|
|
884
|
-
) -> list[dict]:
|
|
885
|
-
if source_file in visited:
|
|
886
|
-
return []
|
|
887
|
-
visited.add(source_file)
|
|
888
|
-
|
|
889
|
-
path = root / source_file
|
|
890
|
-
source = path.read_text()
|
|
891
|
-
sanitized = remove_ts_comments_preserve_literals(source)
|
|
892
|
-
items = list(direct_definitions.get(source_file, {}).values())
|
|
893
|
-
|
|
894
|
-
for match in TS_REEXPORT.finditer(sanitized):
|
|
895
|
-
target_file = resolve_ts_specifier(path, match.group("specifier"), indexed_sources)
|
|
896
|
-
if target_file is None:
|
|
897
|
-
continue
|
|
898
|
-
|
|
899
|
-
form = match.group("form")
|
|
900
|
-
if form == "*":
|
|
901
|
-
items.extend(collect_ts_public_items(root, target_file, module, direct_definitions, indexed_sources, visited))
|
|
902
|
-
continue
|
|
903
|
-
|
|
904
|
-
target_definitions = direct_definitions.get(target_file, {})
|
|
905
|
-
for source_name, public_name in exported_names_from_group(form):
|
|
906
|
-
definition = target_definitions.get(source_name)
|
|
907
|
-
if definition is None:
|
|
908
|
-
items.append(
|
|
909
|
-
{
|
|
910
|
-
"path": public_name,
|
|
911
|
-
"kind": "re-export",
|
|
912
|
-
"name": public_name,
|
|
913
|
-
"source": {
|
|
914
|
-
"path": source_file,
|
|
915
|
-
"line": line_number_from_offset(sanitized, match.start()),
|
|
916
|
-
},
|
|
917
|
-
}
|
|
918
|
-
)
|
|
919
|
-
continue
|
|
920
|
-
items.append({**definition, "path": public_name, "name": public_name})
|
|
921
|
-
|
|
922
|
-
return items
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
def extract_ts_routes(source: str, source_file: str) -> list[dict]:
|
|
926
|
-
sanitized = remove_ts_comments_preserve_literals(source)
|
|
927
|
-
items: list[dict] = []
|
|
928
|
-
|
|
929
|
-
for match in TS_ROUTE_CALL.finditer(sanitized):
|
|
930
|
-
method = match.group("method").upper()
|
|
931
|
-
route_path = match.group("path")
|
|
932
|
-
name = f"{method} {route_path}"
|
|
933
|
-
items.append(
|
|
934
|
-
{
|
|
935
|
-
"path": name,
|
|
936
|
-
"kind": "route",
|
|
937
|
-
"name": name,
|
|
938
|
-
"source": {
|
|
939
|
-
"path": source_file,
|
|
940
|
-
"line": line_number_from_offset(sanitized, match.start()),
|
|
941
|
-
},
|
|
942
|
-
}
|
|
943
|
-
)
|
|
944
|
-
|
|
945
|
-
for match in TS_ROUTE_OBJECT.finditer(sanitized):
|
|
946
|
-
body = match.group("body")
|
|
947
|
-
method_match = TS_ROUTE_METHOD.search(body)
|
|
948
|
-
path_match = TS_ROUTE_URL.search(body)
|
|
949
|
-
if method_match is None or path_match is None:
|
|
950
|
-
continue
|
|
951
|
-
name = f"{method_match.group('method').upper()} {path_match.group('path')}"
|
|
952
|
-
items.append(
|
|
953
|
-
{
|
|
954
|
-
"path": name,
|
|
955
|
-
"kind": "route",
|
|
956
|
-
"name": name,
|
|
957
|
-
"source": {
|
|
958
|
-
"path": source_file,
|
|
959
|
-
"line": line_number_from_offset(sanitized, match.start()),
|
|
960
|
-
},
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Generate .ai/generated/public-surface.json from Rust or TypeScript source files.
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/y;
|
|
9
|
+
const IDENTIFIER_FULL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
const ITEM_KINDS = { fn: "fn", struct: "struct", enum: "enum", trait: "trait" };
|
|
11
|
+
const ITEM_MODIFIERS = new Set(["async", "const", "unsafe", "extern"]);
|
|
12
|
+
const BLOCK_ITEM_KEYWORDS = new Set([
|
|
13
|
+
"fn",
|
|
14
|
+
"struct",
|
|
15
|
+
"enum",
|
|
16
|
+
"trait",
|
|
17
|
+
"impl",
|
|
18
|
+
"mod",
|
|
19
|
+
"const",
|
|
20
|
+
"static",
|
|
21
|
+
"type",
|
|
22
|
+
"union",
|
|
23
|
+
"macro_rules"
|
|
24
|
+
]);
|
|
25
|
+
const TS_SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
26
|
+
const TS_ITEM_KINDS = ["function", "class", "interface", "type", "enum", "variable"];
|
|
27
|
+
const TS_EXPORT_DECLARATION = /\bexport\s+(?:default\s+)?(?:declare\s+)?(?:(?:async\s+)?function\s+(?<function>[A-Za-z_$][A-Za-z0-9_$]*)|class\s+(?<class>[A-Za-z_$][A-Za-z0-9_$]*)|interface\s+(?<interface>[A-Za-z_$][A-Za-z0-9_$]*)|type\s+(?<type>[A-Za-z_$][A-Za-z0-9_$]*)|enum\s+(?<enum>[A-Za-z_$][A-Za-z0-9_$]*)|(?:const|let|var)\s+(?<variable>[A-Za-z_$][A-Za-z0-9_$]*))/g;
|
|
28
|
+
const TS_REEXPORT = /\bexport\s+(?:type\s+)?(?<form>\*|\{[^}]*\})\s+from\s+(?<quote>["'`])(?<specifier>[^"'`]+)\k<quote>/g;
|
|
29
|
+
const TS_ROUTE_CALL = /\.\s*(?<method>get|post|put|patch|delete|options|head)\s*(?:<(?:(?!>\s*\()[\s\S])*>\s*)?\(\s*(?<quote>["'`])(?<path>[^"'`]+)\k<quote>/gi;
|
|
30
|
+
const TS_ROUTE_OBJECT = /\.route\s*\(\s*\{(?<body>.*?)\}\s*\)/gis;
|
|
31
|
+
const TS_ROUTE_METHOD = /\bmethod\s*:\s*(?<quote>["'`])(?<method>[A-Za-z]+)\k<quote>/i;
|
|
32
|
+
const TS_ROUTE_URL = /\b(?:url|path)\s*:\s*(?<quote>["'`])(?<path>[^"'`]+)\k<quote>/i;
|
|
33
|
+
const USAGE = "usage: generate-public-surface [-h] [--check] [--print] [--output OUTPUT]";
|
|
34
|
+
|
|
35
|
+
class ToolExit extends Error {}
|
|
36
|
+
|
|
37
|
+
function fail(message) {
|
|
38
|
+
throw new ToolExit(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isObject(value) {
|
|
42
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function get(container, key, fallback) {
|
|
46
|
+
return isObject(container) && key in container ? container[key] : fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isFile(target) {
|
|
50
|
+
try {
|
|
51
|
+
return fs.statSync(target).isFile();
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readText(target) {
|
|
58
|
+
return fs.readFileSync(target, "utf8").replace(/\r\n?/g, "\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolvePath(target) {
|
|
62
|
+
const absolute = path.resolve(target);
|
|
63
|
+
try {
|
|
64
|
+
return fs.realpathSync(absolute);
|
|
65
|
+
} catch {
|
|
66
|
+
const parent = path.dirname(absolute);
|
|
67
|
+
return parent === absolute ? absolute : path.join(resolvePath(parent), path.basename(absolute));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isInside(relative) {
|
|
72
|
+
return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function relativePath(target, root) {
|
|
76
|
+
let relative = path.relative(path.resolve(root), path.resolve(target));
|
|
77
|
+
if (!isInside(relative)) {
|
|
78
|
+
relative = path.relative(resolvePath(root), resolvePath(target));
|
|
79
|
+
if (!isInside(relative)) {
|
|
80
|
+
fail("'" + target + "' is not in the subpath of '" + root + "'");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return relative === "" ? "." : relative.split(path.sep).join("/");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pathSuffix(target) {
|
|
87
|
+
const name = path.basename(target);
|
|
88
|
+
const index = name.lastIndexOf(".");
|
|
89
|
+
return index > 0 && index < name.length - 1 ? name.slice(index) : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function withSuffix(target, suffix) {
|
|
93
|
+
const current = pathSuffix(target);
|
|
94
|
+
return (current ? target.slice(0, -current.length) : target) + suffix;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isSpace(character) {
|
|
98
|
+
return /\s/.test(character);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isAlpha(character) {
|
|
102
|
+
return /\p{L}/u.test(character);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function collapseWhitespace(text) {
|
|
106
|
+
return text.split(/\s+/).filter(Boolean).join(" ");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function uniqueItems(items) {
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
return items.filter((item) => {
|
|
112
|
+
const key = JSON.stringify([item.path, item.kind, item.source.path, item.source.line]);
|
|
113
|
+
if (seen.has(key)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
seen.add(key);
|
|
117
|
+
return true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function loadModuleIndex(root) {
|
|
122
|
+
const target = path.join(root, ".ai/generated/module-index.json");
|
|
123
|
+
if (!isFile(target)) {
|
|
124
|
+
fail("Missing .ai/generated/module-index.json; run .ai/tools/generate-module-index first.");
|
|
125
|
+
}
|
|
126
|
+
return JSON.parse(readText(target));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loadOptionalJson(target) {
|
|
130
|
+
if (!isFile(target)) {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
return JSON.parse(readText(target));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function lineNumberFromOffset(source, offset) {
|
|
137
|
+
let count = 1;
|
|
138
|
+
for (let index = source.indexOf("\n"); index >= 0 && index < offset; index = source.indexOf("\n", index + 1)) {
|
|
139
|
+
count += 1;
|
|
140
|
+
}
|
|
141
|
+
return count;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function countNewlines(text) {
|
|
145
|
+
return text.split("\n").length - 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rawStringEnd(source, index) {
|
|
149
|
+
for (const prefix of ["br", "rb", "r"]) {
|
|
150
|
+
if (!source.startsWith(prefix, index)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
let cursor = index + prefix.length;
|
|
154
|
+
let hashes = 0;
|
|
155
|
+
while (cursor < source.length && source[cursor] === "#") {
|
|
156
|
+
hashes += 1;
|
|
157
|
+
cursor += 1;
|
|
158
|
+
}
|
|
159
|
+
if (cursor >= source.length || source[cursor] !== "\"") {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const delimiter = "\"" + "#".repeat(hashes);
|
|
163
|
+
const end = source.indexOf(delimiter, cursor + 1);
|
|
164
|
+
return end === -1 ? source.length : end + delimiter.length;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function quotedLiteralEnd(source, index) {
|
|
170
|
+
const quote = source[index];
|
|
171
|
+
let escaped = false;
|
|
172
|
+
for (let cursor = index + 1; cursor < source.length; cursor += 1) {
|
|
173
|
+
const character = source[cursor];
|
|
174
|
+
if (escaped) {
|
|
175
|
+
escaped = false;
|
|
176
|
+
} else if (character === "\\") {
|
|
177
|
+
escaped = true;
|
|
178
|
+
} else if (character === quote) {
|
|
179
|
+
return cursor + 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return source.length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function blockCommentEnd(source, index) {
|
|
186
|
+
let cursor = index + 2;
|
|
187
|
+
let depth = 1;
|
|
188
|
+
while (cursor < source.length && depth) {
|
|
189
|
+
if (source.startsWith("/*", cursor)) {
|
|
190
|
+
depth += 1;
|
|
191
|
+
cursor += 2;
|
|
192
|
+
} else if (source.startsWith("*/", cursor)) {
|
|
193
|
+
depth -= 1;
|
|
194
|
+
cursor += 2;
|
|
195
|
+
} else {
|
|
196
|
+
cursor += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return cursor;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isByteStringStart(source, cursor) {
|
|
203
|
+
return source[cursor] === "\"" || (source[cursor] === "b" && cursor + 1 < source.length && source[cursor + 1] === "\"");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isCharLiteralStart(source, cursor) {
|
|
207
|
+
return source[cursor] === "'" && cursor + 1 < source.length && !isAlpha(source[cursor + 1]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function scanTokens(source) {
|
|
211
|
+
const tokens = [];
|
|
212
|
+
let cursor = 0;
|
|
213
|
+
let line = 1;
|
|
214
|
+
|
|
215
|
+
while (cursor < source.length) {
|
|
216
|
+
const character = source[cursor];
|
|
217
|
+
|
|
218
|
+
if (isSpace(character)) {
|
|
219
|
+
if (character === "\n") {
|
|
220
|
+
line += 1;
|
|
221
|
+
}
|
|
222
|
+
cursor += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (source.startsWith("//", cursor)) {
|
|
227
|
+
const end = source.indexOf("\n", cursor + 2);
|
|
228
|
+
cursor = end === -1 ? source.length : end;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (source.startsWith("/*", cursor)) {
|
|
233
|
+
const end = blockCommentEnd(source, cursor);
|
|
234
|
+
line += countNewlines(source.slice(cursor, end));
|
|
235
|
+
cursor = end;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const rawEnd = rawStringEnd(source, cursor);
|
|
240
|
+
if (rawEnd !== null) {
|
|
241
|
+
line += countNewlines(source.slice(cursor, rawEnd));
|
|
242
|
+
cursor = rawEnd;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isByteStringStart(source, cursor)) {
|
|
247
|
+
const end = quotedLiteralEnd(source, character === "b" ? cursor + 1 : cursor);
|
|
248
|
+
line += countNewlines(source.slice(cursor, end));
|
|
249
|
+
cursor = end;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (isCharLiteralStart(source, cursor)) {
|
|
254
|
+
const end = quotedLiteralEnd(source, cursor);
|
|
255
|
+
line += countNewlines(source.slice(cursor, end));
|
|
256
|
+
cursor = end;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
IDENTIFIER.lastIndex = cursor;
|
|
261
|
+
const match = IDENTIFIER.exec(source);
|
|
262
|
+
if (match) {
|
|
263
|
+
tokens.push({ value: match[0], start: cursor, end: IDENTIFIER.lastIndex, line });
|
|
264
|
+
cursor = IDENTIFIER.lastIndex;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
tokens.push({ value: character, start: cursor, end: cursor + 1, line });
|
|
269
|
+
cursor += 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return tokens;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function removeCommentsPreserveLiterals(text) {
|
|
276
|
+
const output = [];
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
while (cursor < text.length) {
|
|
279
|
+
if (text.startsWith("//", cursor)) {
|
|
280
|
+
const end = text.indexOf("\n", cursor + 2);
|
|
281
|
+
if (end === -1) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
output.push("\n");
|
|
285
|
+
cursor = end + 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (text.startsWith("/*", cursor)) {
|
|
290
|
+
const end = blockCommentEnd(text, cursor);
|
|
291
|
+
output.push("\n".repeat(countNewlines(text.slice(cursor, end))));
|
|
292
|
+
cursor = end;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const rawEnd = rawStringEnd(text, cursor);
|
|
297
|
+
if (rawEnd !== null) {
|
|
298
|
+
output.push(text.slice(cursor, rawEnd));
|
|
299
|
+
cursor = rawEnd;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const character = text[cursor];
|
|
304
|
+
if (isByteStringStart(text, cursor)) {
|
|
305
|
+
const end = quotedLiteralEnd(text, character === "b" ? cursor + 1 : cursor);
|
|
306
|
+
output.push(text.slice(cursor, end));
|
|
307
|
+
cursor = end;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (isCharLiteralStart(text, cursor)) {
|
|
312
|
+
const end = quotedLiteralEnd(text, cursor);
|
|
313
|
+
output.push(text.slice(cursor, end));
|
|
314
|
+
cursor = end;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
output.push(character);
|
|
319
|
+
cursor += 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return output.join("");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeSignature(signature) {
|
|
326
|
+
return collapseWhitespace(removeCommentsPreserveLiterals(signature));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function trackDepth(depth, character) {
|
|
330
|
+
if (character === "<") {
|
|
331
|
+
depth.angle += 1;
|
|
332
|
+
} else if (character === ">" && depth.angle) {
|
|
333
|
+
depth.angle -= 1;
|
|
334
|
+
} else if (character === "(") {
|
|
335
|
+
depth.paren += 1;
|
|
336
|
+
} else if (character === ")" && depth.paren) {
|
|
337
|
+
depth.paren -= 1;
|
|
338
|
+
} else if (character === "[") {
|
|
339
|
+
depth.bracket += 1;
|
|
340
|
+
} else if (character === "]" && depth.bracket) {
|
|
341
|
+
depth.bracket -= 1;
|
|
342
|
+
}
|
|
343
|
+
return depth.angle === 0 && depth.paren === 0 && depth.bracket === 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function signatureFromSource(source, start) {
|
|
347
|
+
let cursor = start;
|
|
348
|
+
const depth = { angle: 0, paren: 0, bracket: 0 };
|
|
349
|
+
|
|
350
|
+
while (cursor < source.length) {
|
|
351
|
+
if (source.startsWith("//", cursor)) {
|
|
352
|
+
const end = source.indexOf("\n", cursor + 2);
|
|
353
|
+
cursor = end === -1 ? source.length : end;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (source.startsWith("/*", cursor)) {
|
|
358
|
+
cursor = blockCommentEnd(source, cursor);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const rawEnd = rawStringEnd(source, cursor);
|
|
363
|
+
if (rawEnd !== null) {
|
|
364
|
+
cursor = rawEnd;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const character = source[cursor];
|
|
369
|
+
if (isByteStringStart(source, cursor)) {
|
|
370
|
+
cursor = quotedLiteralEnd(source, character === "b" ? cursor + 1 : cursor);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (isCharLiteralStart(source, cursor)) {
|
|
375
|
+
cursor = quotedLiteralEnd(source, cursor);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (trackDepth(depth, character) && (character === "{" || character === ";")) {
|
|
380
|
+
return normalizeSignature(source.slice(start, cursor));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
cursor += 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return normalizeSignature(source.slice(start));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function childModuleCandidates(currentFile, moduleName) {
|
|
390
|
+
const base = ["lib.rs", "main.rs", "mod.rs"].includes(path.basename(currentFile))
|
|
391
|
+
? path.dirname(currentFile)
|
|
392
|
+
: withSuffix(currentFile, "");
|
|
393
|
+
return [path.join(base, moduleName + ".rs"), path.join(base, moduleName, "mod.rs")];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isBarePub(tokens, index) {
|
|
397
|
+
return tokens[index].value === "pub" && !(index + 1 < tokens.length && tokens[index + 1].value === "(");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function publicDeclAt(tokens, index) {
|
|
401
|
+
if (!isBarePub(tokens, index)) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let cursor = index + 1;
|
|
406
|
+
while (cursor < tokens.length && ITEM_MODIFIERS.has(tokens[cursor].value)) {
|
|
407
|
+
cursor += 1;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (cursor >= tokens.length) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const kind = tokens[cursor].value;
|
|
415
|
+
if (kind === "mod") {
|
|
416
|
+
if (cursor + 1 < tokens.length && IDENTIFIER_FULL.test(tokens[cursor + 1].value)) {
|
|
417
|
+
return { kind: "mod", name: tokens[cursor + 1].value, pubIndex: index, keywordIndex: cursor, nameIndex: cursor + 1 };
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (Object.hasOwn(ITEM_KINDS, kind) && cursor + 1 < tokens.length) {
|
|
423
|
+
const name = tokens[cursor + 1].value;
|
|
424
|
+
if (IDENTIFIER_FULL.test(name)) {
|
|
425
|
+
return { kind: ITEM_KINDS[kind], name, pubIndex: index, keywordIndex: cursor, nameIndex: cursor + 1 };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function skipBraceBlock(tokens, index) {
|
|
433
|
+
let depth = 0;
|
|
434
|
+
let cursor = index;
|
|
435
|
+
while (cursor < tokens.length) {
|
|
436
|
+
if (tokens[cursor].value === "{") {
|
|
437
|
+
depth += 1;
|
|
438
|
+
} else if (tokens[cursor].value === "}") {
|
|
439
|
+
depth -= 1;
|
|
440
|
+
if (depth === 0) {
|
|
441
|
+
return cursor + 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
cursor += 1;
|
|
445
|
+
}
|
|
446
|
+
return cursor;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function skipItem(tokens, index) {
|
|
450
|
+
for (let cursor = index; cursor < tokens.length; cursor += 1) {
|
|
451
|
+
if (tokens[cursor].value === ";") {
|
|
452
|
+
return cursor + 1;
|
|
453
|
+
}
|
|
454
|
+
if (tokens[cursor].value === "{") {
|
|
455
|
+
return skipBraceBlock(tokens, cursor);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return tokens.length;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function fileModuleParts(sourceFile, module) {
|
|
462
|
+
const modulePath = get(module, "path", ".");
|
|
463
|
+
const prefix = modulePath === "." ? "src/" : modulePath + "/src/";
|
|
464
|
+
if (sourceFile.endsWith("/src/lib.rs") || sourceFile.endsWith("/src/main.rs")) {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
let relative = sourceFile.startsWith(prefix) ? sourceFile.slice(prefix.length) : sourceFile;
|
|
468
|
+
relative = relative.endsWith(".rs") ? relative.slice(0, -3) : relative;
|
|
469
|
+
return relative.split("/").filter((part) => part && part !== "mod");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function joinPath(parts) {
|
|
473
|
+
return parts.join("::");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function publicPath(parts, itemName) {
|
|
477
|
+
return joinPath([...parts, itemName]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function isPathSeparator(tokens, index) {
|
|
481
|
+
return index + 1 < tokens.length && tokens[index].value === ":" && tokens[index + 1].value === ":";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function publicFileModuleNames(source) {
|
|
485
|
+
const tokens = scanTokens(source);
|
|
486
|
+
const names = [];
|
|
487
|
+
tokens.forEach((token, index) => {
|
|
488
|
+
if (token.value !== "pub") {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const decl = publicDeclAt(tokens, index);
|
|
492
|
+
if (decl === null || decl.kind !== "mod") {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (decl.nameIndex + 1 < tokens.length && tokens[decl.nameIndex + 1].value === ";") {
|
|
496
|
+
names.push(decl.name);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
return names;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function parsePubUseExports(source) {
|
|
503
|
+
const tokens = scanTokens(source);
|
|
504
|
+
const exports = [];
|
|
505
|
+
|
|
506
|
+
function parseGroup(index, prefix) {
|
|
507
|
+
let cursor = index + 1;
|
|
508
|
+
while (cursor < tokens.length) {
|
|
509
|
+
if (tokens[cursor].value === "}") {
|
|
510
|
+
return cursor + 1;
|
|
511
|
+
}
|
|
512
|
+
if (tokens[cursor].value === ",") {
|
|
513
|
+
cursor += 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
cursor = parseTree(cursor, prefix);
|
|
517
|
+
if (cursor < tokens.length && tokens[cursor].value === ",") {
|
|
518
|
+
cursor += 1;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return cursor;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function parseTree(index, prefix) {
|
|
525
|
+
let cursor = index;
|
|
526
|
+
const parts = [...prefix];
|
|
527
|
+
|
|
528
|
+
while (cursor < tokens.length) {
|
|
529
|
+
const token = tokens[cursor];
|
|
530
|
+
|
|
531
|
+
if (token.value === "{") {
|
|
532
|
+
return parseGroup(cursor, parts);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (token.value === "*") {
|
|
536
|
+
exports.push({ path: [...parts], alias: null, wildcard: true });
|
|
537
|
+
return cursor + 1;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!IDENTIFIER_FULL.test(token.value)) {
|
|
541
|
+
return cursor + 1;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const name = token.value;
|
|
545
|
+
if (name === "self") {
|
|
546
|
+
cursor += 1;
|
|
547
|
+
if (isPathSeparator(tokens, cursor)) {
|
|
548
|
+
cursor += 2;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
return cursor;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
parts.push(name);
|
|
555
|
+
cursor += 1;
|
|
556
|
+
|
|
557
|
+
if (isPathSeparator(tokens, cursor)) {
|
|
558
|
+
cursor += 2;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let alias = null;
|
|
563
|
+
if (cursor + 1 < tokens.length && tokens[cursor].value === "as") {
|
|
564
|
+
if (IDENTIFIER_FULL.test(tokens[cursor + 1].value)) {
|
|
565
|
+
alias = tokens[cursor + 1].value;
|
|
566
|
+
cursor += 2;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
exports.push({ path: [...parts], alias, wildcard: false });
|
|
571
|
+
return cursor;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return cursor;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
let index = 0;
|
|
578
|
+
while (index < tokens.length) {
|
|
579
|
+
if (
|
|
580
|
+
tokens[index].value === "pub"
|
|
581
|
+
&& isBarePub(tokens, index)
|
|
582
|
+
&& index + 1 < tokens.length
|
|
583
|
+
&& tokens[index + 1].value === "use"
|
|
584
|
+
) {
|
|
585
|
+
let cursor = parseTree(index + 2, []);
|
|
586
|
+
while (cursor < tokens.length && tokens[cursor].value !== ";") {
|
|
587
|
+
cursor += 1;
|
|
588
|
+
}
|
|
589
|
+
index = cursor + 1;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
index += 1;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return exports;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function definesPath(parts, definitions) {
|
|
599
|
+
const joined = joinPath(parts);
|
|
600
|
+
return definitions.has(joined) || [...definitions.keys()].some((key) => key.startsWith(joined + "::"));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function resolveUsePath(parts, currentParts, definitions) {
|
|
604
|
+
if (parts.length === 0) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (parts[0] === "crate") {
|
|
609
|
+
return parts.slice(1);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (parts[0] === "self") {
|
|
613
|
+
return [...currentParts, ...parts.slice(1)];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (parts[0] === "super") {
|
|
617
|
+
let cursor = 0;
|
|
618
|
+
const base = [...currentParts];
|
|
619
|
+
while (cursor < parts.length && parts[cursor] === "super") {
|
|
620
|
+
base.pop();
|
|
621
|
+
cursor += 1;
|
|
622
|
+
}
|
|
623
|
+
return [...base, ...parts.slice(cursor)];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (definesPath(parts, definitions)) {
|
|
627
|
+
return parts;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const relative = [...currentParts, ...parts];
|
|
631
|
+
if (definesPath(relative, definitions)) {
|
|
632
|
+
return relative;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function reexportedItemsFromSource(source, sourceFile, module, definitions) {
|
|
639
|
+
const items = [];
|
|
640
|
+
const currentParts = fileModuleParts(sourceFile, module);
|
|
641
|
+
|
|
642
|
+
for (const exported of parsePubUseExports(source)) {
|
|
643
|
+
const targetParts = resolveUsePath(exported.path, currentParts, definitions);
|
|
644
|
+
if (targetParts === null) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const targetPath = joinPath(targetParts);
|
|
649
|
+
|
|
650
|
+
if (exported.wildcard) {
|
|
651
|
+
const prefix = targetPath + "::";
|
|
652
|
+
for (const [definitionPath, definition] of definitions) {
|
|
653
|
+
if (!definitionPath.startsWith(prefix) || definitionPath.slice(prefix.length).includes("::")) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
items.push({ ...definition, path: publicPath(currentParts, definition.name) });
|
|
657
|
+
}
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const definition = definitions.get(targetPath);
|
|
662
|
+
if (definition === undefined) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const name = exported.alias || definition.name;
|
|
666
|
+
items.push({ ...definition, path: publicPath(currentParts, name), name });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return items;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function moduleSourceFiles(module) {
|
|
673
|
+
return get(get(module, "files", {}), "source", []);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function moduleRoot(root, module) {
|
|
677
|
+
const modulePath = get(module, "path", ".");
|
|
678
|
+
return modulePath === "." ? root : path.join(root, modulePath);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function indexedSourceMap(root, module) {
|
|
682
|
+
return new Map(moduleSourceFiles(module).map((sourceFile) => [resolvePath(path.join(root, sourceFile)), sourceFile]));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function reachablePublicSourceFiles(root, module) {
|
|
686
|
+
const indexedSources = indexedSourceMap(root, module);
|
|
687
|
+
const crateRoots = [path.join(moduleRoot(root, module), "src/lib.rs"), path.join(moduleRoot(root, module), "src/main.rs")];
|
|
688
|
+
const reachable = [];
|
|
689
|
+
const visited = new Set();
|
|
690
|
+
|
|
691
|
+
function visit(target) {
|
|
692
|
+
const resolved = resolvePath(target);
|
|
693
|
+
if (visited.has(resolved) || !indexedSources.has(resolved)) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
visited.add(resolved);
|
|
697
|
+
reachable.push(indexedSources.get(resolved));
|
|
698
|
+
|
|
699
|
+
for (const moduleName of publicFileModuleNames(readText(target))) {
|
|
700
|
+
for (const candidate of childModuleCandidates(target, moduleName)) {
|
|
701
|
+
if (indexedSources.has(resolvePath(candidate))) {
|
|
702
|
+
visit(candidate);
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
for (const crateRoot of crateRoots) {
|
|
710
|
+
if (indexedSources.has(resolvePath(crateRoot))) {
|
|
711
|
+
visit(crateRoot);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return reachable;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function extractItemsFromSource(source, sourceFile, module) {
|
|
720
|
+
const tokens = scanTokens(source);
|
|
721
|
+
const items = [];
|
|
722
|
+
|
|
723
|
+
function parseScope(startIndex, pathParts, stopAtBrace) {
|
|
724
|
+
let cursor = startIndex;
|
|
725
|
+
while (cursor < tokens.length) {
|
|
726
|
+
const token = tokens[cursor];
|
|
727
|
+
|
|
728
|
+
if (stopAtBrace && token.value === "}") {
|
|
729
|
+
return cursor + 1;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (token.value === "pub") {
|
|
733
|
+
const decl = publicDeclAt(tokens, cursor);
|
|
734
|
+
if (decl !== null) {
|
|
735
|
+
const afterName = decl.nameIndex + 1;
|
|
736
|
+
if (decl.kind === "mod") {
|
|
737
|
+
if (afterName < tokens.length && tokens[afterName].value === "{") {
|
|
738
|
+
cursor = parseScope(afterName + 1, [...pathParts, decl.name], true);
|
|
739
|
+
continue;
|
|
961
740
|
}
|
|
962
|
-
|
|
741
|
+
cursor = skipItem(tokens, cursor);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
items.push({
|
|
746
|
+
path: publicPath(pathParts, decl.name),
|
|
747
|
+
kind: decl.kind,
|
|
748
|
+
name: decl.name,
|
|
749
|
+
source: { path: sourceFile, line: tokens[decl.pubIndex].line },
|
|
750
|
+
signature: signatureFromSource(source, tokens[decl.pubIndex].start)
|
|
751
|
+
});
|
|
752
|
+
cursor = skipItem(tokens, cursor);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (BLOCK_ITEM_KEYWORDS.has(token.value)) {
|
|
758
|
+
cursor = skipItem(tokens, cursor);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (token.value === "{") {
|
|
763
|
+
cursor = skipBraceBlock(tokens, cursor);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
963
766
|
|
|
964
|
-
|
|
767
|
+
cursor += 1;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return cursor;
|
|
771
|
+
}
|
|
965
772
|
|
|
773
|
+
parseScope(0, fileModuleParts(sourceFile, module), false);
|
|
774
|
+
return items;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function readModuleSource(root, sourceFile) {
|
|
778
|
+
const target = path.join(root, sourceFile);
|
|
779
|
+
if (!isFile(target)) {
|
|
780
|
+
fail("Missing source file from module index: " + sourceFile);
|
|
781
|
+
}
|
|
782
|
+
return readText(target);
|
|
783
|
+
}
|
|
966
784
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
785
|
+
function extractPublicItems(root, module) {
|
|
786
|
+
const definitions = new Map();
|
|
787
|
+
for (const sourceFile of moduleSourceFiles(module)) {
|
|
788
|
+
for (const item of extractItemsFromSource(readModuleSource(root, sourceFile), sourceFile, module)) {
|
|
789
|
+
if (!definitions.has(item.path)) {
|
|
790
|
+
definitions.set(item.path, item);
|
|
791
|
+
}
|
|
971
792
|
}
|
|
972
|
-
|
|
793
|
+
}
|
|
973
794
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
795
|
+
const items = [];
|
|
796
|
+
for (const sourceFile of reachablePublicSourceFiles(root, module)) {
|
|
797
|
+
const source = readModuleSource(root, sourceFile);
|
|
798
|
+
items.push(...extractItemsFromSource(source, sourceFile, module));
|
|
799
|
+
items.push(...reexportedItemsFromSource(source, sourceFile, module, definitions));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return uniqueItems(items);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function removeTsCommentsPreserveLiterals(source) {
|
|
806
|
+
const output = [];
|
|
807
|
+
let cursor = 0;
|
|
808
|
+
let quote = null;
|
|
809
|
+
let escaped = false;
|
|
810
|
+
|
|
811
|
+
while (cursor < source.length) {
|
|
812
|
+
const character = source[cursor];
|
|
813
|
+
|
|
814
|
+
if (quote !== null) {
|
|
815
|
+
output.push(character);
|
|
816
|
+
if (escaped) {
|
|
817
|
+
escaped = false;
|
|
818
|
+
} else if (character === "\\") {
|
|
819
|
+
escaped = true;
|
|
820
|
+
} else if (character === quote) {
|
|
821
|
+
quote = null;
|
|
822
|
+
}
|
|
823
|
+
cursor += 1;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (source.startsWith("//", cursor)) {
|
|
828
|
+
const end = source.indexOf("\n", cursor + 2);
|
|
829
|
+
if (end === -1) {
|
|
830
|
+
output.push(" ".repeat(source.length - cursor));
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
output.push(" ".repeat(end - cursor));
|
|
834
|
+
output.push("\n");
|
|
835
|
+
cursor = end + 1;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (source.startsWith("/*", cursor)) {
|
|
840
|
+
const found = source.indexOf("*/", cursor + 2);
|
|
841
|
+
const end = found === -1 ? source.length : found + 2;
|
|
842
|
+
output.push(source.slice(cursor, end).replace(/[^\n]/g, " "));
|
|
843
|
+
cursor = end;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (character === "'" || character === "\"" || character === "`") {
|
|
848
|
+
quote = character;
|
|
849
|
+
escaped = false;
|
|
850
|
+
output.push(character);
|
|
851
|
+
cursor += 1;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
output.push(character);
|
|
856
|
+
cursor += 1;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return output.join("");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function tsSignatureFromSource(source, start) {
|
|
863
|
+
let cursor = start;
|
|
864
|
+
const depth = { angle: 0, paren: 0, bracket: 0 };
|
|
865
|
+
let backtickSource = null;
|
|
866
|
+
|
|
867
|
+
while (cursor < source.length) {
|
|
868
|
+
const character = source[cursor];
|
|
869
|
+
if (character === "'" || character === "\"") {
|
|
870
|
+
cursor = quotedLiteralEnd(source, cursor);
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (character === "`") {
|
|
874
|
+
backtickSource = backtickSource || source.replace(/`/g, "\"");
|
|
875
|
+
cursor = quotedLiteralEnd(backtickSource, cursor);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (trackDepth(depth, character) && (character === "{" || character === ";")) {
|
|
880
|
+
return collapseWhitespace(source.slice(start, cursor));
|
|
881
|
+
}
|
|
882
|
+
cursor += 1;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return collapseWhitespace(source.slice(start));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function tsItemKind(match) {
|
|
889
|
+
for (const kind of TS_ITEM_KINDS) {
|
|
890
|
+
const name = match.groups[kind];
|
|
891
|
+
if (name) {
|
|
892
|
+
return [kind === "variable" ? "const" : kind, name];
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function extractTsExportedDeclarations(source, sourceFile) {
|
|
899
|
+
const sanitized = removeTsCommentsPreserveLiterals(source);
|
|
900
|
+
const items = [];
|
|
901
|
+
|
|
902
|
+
for (const match of sanitized.matchAll(TS_EXPORT_DECLARATION)) {
|
|
903
|
+
const kindAndName = tsItemKind(match);
|
|
904
|
+
if (kindAndName === null) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const [kind, name] = kindAndName;
|
|
908
|
+
items.push({
|
|
909
|
+
path: name,
|
|
910
|
+
kind,
|
|
911
|
+
name,
|
|
912
|
+
source: { path: sourceFile, line: lineNumberFromOffset(sanitized, match.index) },
|
|
913
|
+
signature: tsSignatureFromSource(source, match.index)
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return items;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function exportedNamesFromGroup(group) {
|
|
921
|
+
const names = [];
|
|
922
|
+
const body = group.trim().slice(1, -1);
|
|
923
|
+
for (const rawPart of body.split(",")) {
|
|
924
|
+
let part = rawPart.trim();
|
|
925
|
+
if (!part) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (part.startsWith("type ")) {
|
|
929
|
+
part = part.slice("type ".length).trim();
|
|
930
|
+
}
|
|
931
|
+
const separator = /\s+as\s+/.exec(part);
|
|
932
|
+
const pieces = separator
|
|
933
|
+
? [part.slice(0, separator.index).trim(), part.slice(separator.index + separator[0].length).trim()]
|
|
934
|
+
: [part.trim()];
|
|
935
|
+
const sourceName = pieces[0];
|
|
936
|
+
const publicName = pieces.length === 2 ? pieces[1] : sourceName;
|
|
937
|
+
if (sourceName && sourceName !== "default") {
|
|
938
|
+
names.push([sourceName, publicName]);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return names;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function resolveTsSpecifier(currentFile, specifier, indexedSources) {
|
|
945
|
+
if (!specifier.startsWith(".")) {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const base = resolvePath(path.join(path.dirname(currentFile), specifier));
|
|
950
|
+
const candidates = [];
|
|
951
|
+
|
|
952
|
+
if (pathSuffix(base)) {
|
|
953
|
+
candidates.push(base);
|
|
954
|
+
for (const extension of TS_SOURCE_EXTENSIONS) {
|
|
955
|
+
candidates.push(withSuffix(base, extension));
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
for (const extension of TS_SOURCE_EXTENSIONS) {
|
|
959
|
+
candidates.push(withSuffix(base, extension));
|
|
960
|
+
}
|
|
961
|
+
for (const extension of TS_SOURCE_EXTENSIONS) {
|
|
962
|
+
candidates.push(path.join(base, "index" + extension));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
for (const candidate of candidates) {
|
|
967
|
+
const sourceFile = indexedSources.get(resolvePath(candidate));
|
|
968
|
+
if (sourceFile !== undefined) {
|
|
969
|
+
return sourceFile;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function packageExportTargets(value) {
|
|
977
|
+
if (typeof value === "string") {
|
|
978
|
+
return [value];
|
|
979
|
+
}
|
|
980
|
+
if (Array.isArray(value)) {
|
|
981
|
+
return value.flatMap(packageExportTargets);
|
|
982
|
+
}
|
|
983
|
+
if (isObject(value)) {
|
|
984
|
+
return Object.entries(value)
|
|
985
|
+
.filter(([key]) => ["types", "import", "require", "default", "module"].includes(key) || key.startsWith("."))
|
|
986
|
+
.flatMap(([, item]) => packageExportTargets(item));
|
|
987
|
+
}
|
|
988
|
+
return [];
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function tsEntrypointFiles(root, module) {
|
|
992
|
+
const indexedSources = new Set(moduleSourceFiles(module));
|
|
993
|
+
const modulePath = get(module, "path", ".");
|
|
994
|
+
const base = moduleRoot(root, module);
|
|
995
|
+
const manifest = loadOptionalJson(path.join(root, get(module, "manifest", "")));
|
|
996
|
+
const targets = ["exports", "main", "module", "types"].flatMap((key) => packageExportTargets(get(manifest, key, null)));
|
|
997
|
+
|
|
998
|
+
const candidates = [];
|
|
999
|
+
for (const target of targets) {
|
|
1000
|
+
const targetPath = resolvePath(path.resolve(base, target));
|
|
1001
|
+
for (const candidate of [targetPath, ...TS_SOURCE_EXTENSIONS.map((extension) => withSuffix(targetPath, extension))]) {
|
|
1002
|
+
const relative = relativePath(candidate, root);
|
|
1003
|
+
if (indexedSources.has(relative)) {
|
|
1004
|
+
candidates.push(relative);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (candidates.length === 0) {
|
|
1010
|
+
for (const name of ["index", "main", "app"]) {
|
|
1011
|
+
for (const extension of TS_SOURCE_EXTENSIONS) {
|
|
1012
|
+
const relative = relativePath(path.join(base, "src", name + extension), root);
|
|
1013
|
+
if (indexedSources.has(relative)) {
|
|
1014
|
+
candidates.push(relative);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (candidates.length === 0 && modulePath.startsWith("apps/")) {
|
|
1021
|
+
candidates.push(...moduleSourceFiles(module));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return [...new Set(candidates)].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function collectTsPublicItems(root, sourceFile, module, directDefinitions, indexedSources, visited) {
|
|
1028
|
+
if (visited.has(sourceFile)) {
|
|
1029
|
+
return [];
|
|
1030
|
+
}
|
|
1031
|
+
visited.add(sourceFile);
|
|
1032
|
+
|
|
1033
|
+
const target = path.join(root, sourceFile);
|
|
1034
|
+
const sanitized = removeTsCommentsPreserveLiterals(readText(target));
|
|
1035
|
+
const items = [...(directDefinitions.get(sourceFile) || new Map()).values()];
|
|
1036
|
+
|
|
1037
|
+
for (const match of sanitized.matchAll(TS_REEXPORT)) {
|
|
1038
|
+
const targetFile = resolveTsSpecifier(target, match.groups.specifier, indexedSources);
|
|
1039
|
+
if (targetFile === null) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const form = match.groups.form;
|
|
1044
|
+
if (form === "*") {
|
|
1045
|
+
items.push(...collectTsPublicItems(root, targetFile, module, directDefinitions, indexedSources, visited));
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const targetDefinitions = directDefinitions.get(targetFile) || new Map();
|
|
1050
|
+
for (const [sourceName, publicName] of exportedNamesFromGroup(form)) {
|
|
1051
|
+
const definition = targetDefinitions.get(sourceName);
|
|
1052
|
+
if (definition === undefined) {
|
|
1053
|
+
items.push({
|
|
1054
|
+
path: publicName,
|
|
1055
|
+
kind: "re-export",
|
|
1056
|
+
name: publicName,
|
|
1057
|
+
source: { path: sourceFile, line: lineNumberFromOffset(sanitized, match.index) }
|
|
1058
|
+
});
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
items.push({ ...definition, path: publicName, name: publicName });
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return items;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function routeItem(name, sourceFile, sanitized, offset) {
|
|
1069
|
+
return {
|
|
1070
|
+
path: name,
|
|
1071
|
+
kind: "route",
|
|
1072
|
+
name,
|
|
1073
|
+
source: { path: sourceFile, line: lineNumberFromOffset(sanitized, offset) }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function extractTsRoutes(source, sourceFile) {
|
|
1078
|
+
const sanitized = removeTsCommentsPreserveLiterals(source);
|
|
1079
|
+
const items = [];
|
|
1080
|
+
|
|
1081
|
+
for (const match of sanitized.matchAll(TS_ROUTE_CALL)) {
|
|
1082
|
+
items.push(routeItem(match.groups.method.toUpperCase() + " " + match.groups.path, sourceFile, sanitized, match.index));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
for (const match of sanitized.matchAll(TS_ROUTE_OBJECT)) {
|
|
1086
|
+
const body = match.groups.body;
|
|
1087
|
+
const methodMatch = TS_ROUTE_METHOD.exec(body);
|
|
1088
|
+
const pathMatch = TS_ROUTE_URL.exec(body);
|
|
1089
|
+
if (methodMatch === null || pathMatch === null) {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
items.push(routeItem(methodMatch.groups.method.toUpperCase() + " " + pathMatch.groups.path, sourceFile, sanitized, match.index));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return items;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function extractTypescriptPublicItems(root, module) {
|
|
1099
|
+
const indexedSources = indexedSourceMap(root, module);
|
|
1100
|
+
const directDefinitions = new Map();
|
|
1101
|
+
|
|
1102
|
+
for (const sourceFile of moduleSourceFiles(module)) {
|
|
1103
|
+
const definitions = new Map();
|
|
1104
|
+
for (const item of extractTsExportedDeclarations(readModuleSource(root, sourceFile), sourceFile)) {
|
|
1105
|
+
definitions.set(item.name, item);
|
|
1106
|
+
}
|
|
1107
|
+
directDefinitions.set(sourceFile, definitions);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
let entrypoints = tsEntrypointFiles(root, module);
|
|
1111
|
+
if (entrypoints.length === 0) {
|
|
1112
|
+
entrypoints = [...moduleSourceFiles(module)];
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const items = [];
|
|
1116
|
+
for (const entrypoint of entrypoints) {
|
|
1117
|
+
items.push(...collectTsPublicItems(root, entrypoint, module, directDefinitions, indexedSources, new Set()));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (get(module, "path", "").startsWith("apps/")) {
|
|
1121
|
+
for (const sourceFile of moduleSourceFiles(module)) {
|
|
1122
|
+
items.push(...extractTsRoutes(readText(path.join(root, sourceFile)), sourceFile));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return uniqueItems(items);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function moduleIndexIsTypescript(moduleIndex) {
|
|
1130
|
+
const workspace = get(moduleIndex, "workspace", {});
|
|
1131
|
+
const workspaceType = get(workspace, "type", null);
|
|
1132
|
+
if (workspaceType === "npm-workspaces" || workspaceType === "npm-package") {
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
const manifest = get(workspace, "manifest", null);
|
|
1136
|
+
return typeof manifest === "string" && manifest.endsWith("package.json");
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function buildSurface(root) {
|
|
1140
|
+
const moduleIndex = loadModuleIndex(root);
|
|
1141
|
+
const isTypescript = moduleIndexIsTypescript(moduleIndex);
|
|
1142
|
+
const modules = [];
|
|
1143
|
+
|
|
1144
|
+
for (const layer of get(moduleIndex, "layers", [])) {
|
|
1145
|
+
for (const module of get(layer, "modules", [])) {
|
|
1146
|
+
const publicItems = isTypescript || get(module, "language", null) === "typescript"
|
|
1147
|
+
? extractTypescriptPublicItems(root, module)
|
|
1148
|
+
: extractPublicItems(root, module);
|
|
1149
|
+
modules.push({ name: module.name, items: publicItems });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return {
|
|
1154
|
+
schemaVersion: 1,
|
|
1155
|
+
kind: "public-surface",
|
|
1156
|
+
generatedBy: ".ai/tools/generate-public-surface",
|
|
1157
|
+
visibility: isTypescript ? "project-public" : "crate-external",
|
|
1158
|
+
modules
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Matches Python json.dumps(indent=2) with ensure_ascii.
|
|
1163
|
+
function toJson(data) {
|
|
1164
|
+
return JSON.stringify(data, null, 2)
|
|
1165
|
+
.replace(/[\u007f-]/g, (character) => "\\u" + character.charCodeAt(0).toString(16).padStart(4, "0")) + "\n";
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function parseArgs(argv) {
|
|
1169
|
+
const args = { check: false, print: false, output: ".ai/generated/public-surface.json" };
|
|
1170
|
+
const usageError = (message) => {
|
|
1171
|
+
process.stderr.write(USAGE + "\ngenerate-public-surface: error: " + message + "\n");
|
|
1172
|
+
process.exit(2);
|
|
1173
|
+
};
|
|
1174
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1175
|
+
const argument = argv[index];
|
|
1176
|
+
if (argument === "-h" || argument === "--help") {
|
|
1177
|
+
process.stdout.write(USAGE + "\n\nGenerate .ai/generated/public-surface.json from Rust or TypeScript source files.\n");
|
|
1178
|
+
process.exit(0);
|
|
1179
|
+
} else if (argument === "--check") {
|
|
1180
|
+
args.check = true;
|
|
1181
|
+
} else if (argument === "--print") {
|
|
1182
|
+
args.print = true;
|
|
1183
|
+
} else if (argument === "--output" || argument.startsWith("--output=")) {
|
|
1184
|
+
if (argument === "--output") {
|
|
1185
|
+
if (index + 1 >= argv.length) {
|
|
1186
|
+
usageError("argument --output: expected one argument");
|
|
981
1187
|
}
|
|
1188
|
+
index += 1;
|
|
1189
|
+
args.output = argv[index];
|
|
1190
|
+
} else {
|
|
1191
|
+
args.output = argument.slice("--output=".length);
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
usageError("unrecognized arguments: " + argv.slice(index).join(" "));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return args;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function main(argv) {
|
|
1201
|
+
const args = parseArgs(argv);
|
|
1202
|
+
const root = path.resolve(path.dirname(fs.realpathSync(__filename)), "..", "..");
|
|
1203
|
+
const generated = toJson(buildSurface(root));
|
|
1204
|
+
const outputPath = path.join(root, args.output);
|
|
1205
|
+
const displayPath = path.relative(root, outputPath).split(path.sep).join("/");
|
|
1206
|
+
|
|
1207
|
+
if (args.check) {
|
|
1208
|
+
if (!isFile(outputPath)) {
|
|
1209
|
+
process.stderr.write("Missing generated public surface: " + displayPath + "\n");
|
|
1210
|
+
return 1;
|
|
1211
|
+
}
|
|
1212
|
+
if (readText(outputPath) !== generated) {
|
|
1213
|
+
process.stderr.write("Stale generated public surface: " + displayPath + "\n");
|
|
1214
|
+
return 1;
|
|
1215
|
+
}
|
|
1216
|
+
process.stdout.write("generate-public-surface check passed\n");
|
|
1217
|
+
return 0;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (args.print) {
|
|
1221
|
+
process.stdout.write(generated);
|
|
1222
|
+
return 0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1226
|
+
fs.writeFileSync(outputPath, generated);
|
|
1227
|
+
process.stdout.write("wrote " + displayPath + "\n");
|
|
1228
|
+
return 0;
|
|
1229
|
+
}
|
|
982
1230
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
for source_file in module.get("files", {}).get("source", []):
|
|
993
|
-
items.extend(extract_ts_routes((root / source_file).read_text(), source_file))
|
|
994
|
-
|
|
995
|
-
seen: set[tuple[str, str, str, int]] = set()
|
|
996
|
-
unique_items: list[dict] = []
|
|
997
|
-
for item in items:
|
|
998
|
-
key = (item["path"], item["kind"], item["source"]["path"], item["source"]["line"])
|
|
999
|
-
if key in seen:
|
|
1000
|
-
continue
|
|
1001
|
-
seen.add(key)
|
|
1002
|
-
unique_items.append(item)
|
|
1003
|
-
|
|
1004
|
-
return unique_items
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
def module_index_is_typescript(module_index: dict) -> bool:
|
|
1008
|
-
workspace = module_index.get("workspace", {})
|
|
1009
|
-
workspace_type = workspace.get("type")
|
|
1010
|
-
if workspace_type in {"npm-workspaces", "npm-package"}:
|
|
1011
|
-
return True
|
|
1012
|
-
manifest = workspace.get("manifest")
|
|
1013
|
-
return isinstance(manifest, str) and manifest.endswith("package.json")
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
def build_surface(root: Path) -> dict:
|
|
1017
|
-
module_index = load_module_index(root)
|
|
1018
|
-
is_typescript = module_index_is_typescript(module_index)
|
|
1019
|
-
modules = []
|
|
1020
|
-
|
|
1021
|
-
for layer in module_index.get("layers", []):
|
|
1022
|
-
for module in layer.get("modules", []):
|
|
1023
|
-
public_items = (
|
|
1024
|
-
extract_typescript_public_items(root, module)
|
|
1025
|
-
if is_typescript or module.get("language") == "typescript"
|
|
1026
|
-
else extract_public_items(root, module)
|
|
1027
|
-
)
|
|
1028
|
-
modules.append(
|
|
1029
|
-
{
|
|
1030
|
-
"name": module["name"],
|
|
1031
|
-
"items": public_items,
|
|
1032
|
-
}
|
|
1033
|
-
)
|
|
1034
|
-
|
|
1035
|
-
return {
|
|
1036
|
-
"schemaVersion": 1,
|
|
1037
|
-
"kind": "public-surface",
|
|
1038
|
-
"generatedBy": ".ai/tools/generate-public-surface",
|
|
1039
|
-
"visibility": "project-public" if is_typescript else "crate-external",
|
|
1040
|
-
"modules": modules,
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
def main() -> int:
|
|
1045
|
-
parser = argparse.ArgumentParser(description="Generate .ai/generated/public-surface.json from Rust or TypeScript source files.")
|
|
1046
|
-
parser.add_argument("--check", action="store_true", help="Fail if the generated public surface differs from the current file.")
|
|
1047
|
-
parser.add_argument("--print", action="store_true", help="Print generated JSON instead of writing it.")
|
|
1048
|
-
parser.add_argument("--output", default=".ai/generated/public-surface.json", help="Output path relative to the project root.")
|
|
1049
|
-
args = parser.parse_args()
|
|
1050
|
-
|
|
1051
|
-
root = Path(__file__).resolve().parents[2]
|
|
1052
|
-
generated = json.dumps(build_surface(root), indent=2, sort_keys=False) + "\n"
|
|
1053
|
-
output_path = root / args.output
|
|
1054
|
-
|
|
1055
|
-
if args.check:
|
|
1056
|
-
if not output_path.is_file():
|
|
1057
|
-
sys.stderr.write(f"Missing generated public surface: {output_path.relative_to(root)}\n")
|
|
1058
|
-
return 1
|
|
1059
|
-
current = output_path.read_text()
|
|
1060
|
-
if current != generated:
|
|
1061
|
-
sys.stderr.write(f"Stale generated public surface: {output_path.relative_to(root)}\n")
|
|
1062
|
-
return 1
|
|
1063
|
-
print("generate-public-surface check passed")
|
|
1064
|
-
return 0
|
|
1065
|
-
|
|
1066
|
-
if args.print:
|
|
1067
|
-
sys.stdout.write(generated)
|
|
1068
|
-
return 0
|
|
1069
|
-
|
|
1070
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1071
|
-
output_path.write_text(generated)
|
|
1072
|
-
print(f"wrote {output_path.relative_to(root)}")
|
|
1073
|
-
return 0
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
if __name__ == "__main__":
|
|
1077
|
-
raise SystemExit(main())
|
|
1231
|
+
try {
|
|
1232
|
+
process.exitCode = main(process.argv.slice(2));
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (!(error instanceof ToolExit)) {
|
|
1235
|
+
throw error;
|
|
1236
|
+
}
|
|
1237
|
+
process.stderr.write(error.message + "\n");
|
|
1238
|
+
process.exitCode = 1;
|
|
1239
|
+
}
|