create-visualbuild-app 0.1.0 → 1.0.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 +306 -123
- package/docs/user-guide.md +759 -0
- package/package.json +92 -85
- package/scripts/create-builder-app.mjs +513 -501
- package/scripts/vb-dev.mjs +41 -32
- package/scripts/vb-generate.mjs +332 -224
- package/templates/default/app/.vercelignore +6 -0
- package/templates/default/app/README.md +56 -21
- package/templates/default/app/visualbuild/components.json +3 -0
- package/templates/default/app/visualbuild/config.d.ts +48 -0
- package/templates/default/app/visualbuild.config.ts +38 -0
- package/templates/default/builder/README.md +37 -21
- package/visual-app-builder/server/parseReactPage.ts +776 -571
- package/visual-app-builder/shared/createReactComponentSource.d.mts +2 -0
- package/visual-app-builder/shared/createReactComponentSource.mjs +45 -0
- package/visual-app-builder/shared/generateReactSource.d.mts +57 -37
- package/visual-app-builder/shared/generateReactSource.mjs +608 -443
- package/visual-app-builder/shared/npmBuildCommand.d.mts +17 -0
- package/visual-app-builder/shared/npmBuildCommand.mjs +26 -0
- package/visual-app-builder/shared/syncCustomComponentSource.d.mts +13 -0
- package/visual-app-builder/shared/syncCustomComponentSource.mjs +345 -0
- package/visual-app-builder/shared/vercelDeployment.d.mts +31 -0
- package/visual-app-builder/shared/vercelDeployment.mjs +266 -0
- package/visual-app-builder/shared/visualbuildConfig.d.mts +144 -0
- package/visual-app-builder/shared/visualbuildConfig.mjs +578 -0
- package/visual-app-builder/src/App.tsx +1090 -874
- package/visual-app-builder/src/components/Canvas.tsx +1116 -1059
- package/visual-app-builder/src/components/CodePanel.tsx +1147 -812
- package/visual-app-builder/src/components/ComponentSidebar.tsx +365 -302
- package/visual-app-builder/src/components/DeploymentModal.tsx +295 -0
- package/visual-app-builder/src/components/PageSwitcher.tsx +133 -133
- package/visual-app-builder/src/components/ProjectExplorer.tsx +1054 -1054
- package/visual-app-builder/src/components/PropertiesPanel.tsx +792 -692
- package/visual-app-builder/src/components/Topbar.tsx +257 -128
- package/visual-app-builder/src/data/componentRegistry.tsx +613 -292
- package/visual-app-builder/src/data/tailwindClassCatalog.ts +95 -0
- package/visual-app-builder/src/index.css +383 -111
- package/visual-app-builder/src/stores/useAppStore.ts +2385 -1265
- package/visual-app-builder/src/theme.ts +71 -0
- package/visual-app-builder/src/types/index.ts +350 -261
- package/visual-app-builder/src/utils/codegen.ts +90 -66
- package/visual-app-builder/src/utils/deployment.ts +54 -0
- package/visual-app-builder/src/utils/projectPersistence.ts +218 -177
- package/visual-app-builder/vite.config.ts +1946 -1479
|
@@ -1,1479 +1,1946 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process'
|
|
2
|
-
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import fs from 'node:fs/promises'
|
|
4
|
-
import type { ServerResponse } from 'node:http'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import { promisify } from 'node:util'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
path: string
|
|
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
|
-
const
|
|
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
|
-
throw new Error('
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return
|
|
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
|
-
path: projectPath.relativePath,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
)
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
)
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
.
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
})
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import type { ServerResponse } from 'node:http'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
import { parse } from '@babel/parser'
|
|
8
|
+
import { watch as watchFiles } from 'chokidar'
|
|
9
|
+
import { defineConfig } from 'vite'
|
|
10
|
+
import type { Plugin } from 'vite'
|
|
11
|
+
import react from '@vitejs/plugin-react'
|
|
12
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
13
|
+
import { loadVisualBuildConfig } from './shared/visualbuildConfig.mjs'
|
|
14
|
+
import { createReactComponentSource } from './shared/createReactComponentSource.mjs'
|
|
15
|
+
import { resolveNpmBuildCommand } from './shared/npmBuildCommand.mjs'
|
|
16
|
+
import {
|
|
17
|
+
deployStaticOutputToVercel,
|
|
18
|
+
validateVercelProjectName,
|
|
19
|
+
validateVercelTeamId,
|
|
20
|
+
} from './shared/vercelDeployment.mjs'
|
|
21
|
+
import {
|
|
22
|
+
parseReactComponentPreview,
|
|
23
|
+
parseReactPageSource,
|
|
24
|
+
reconcileImportedTree,
|
|
25
|
+
visualTreesEqual,
|
|
26
|
+
type ImportedPageTree,
|
|
27
|
+
type ImportedVisualNode,
|
|
28
|
+
} from './server/parseReactPage'
|
|
29
|
+
|
|
30
|
+
const execFileAsync = promisify(execFile)
|
|
31
|
+
const builderPackageRoot = process.cwd()
|
|
32
|
+
const projectRoot = path.resolve(
|
|
33
|
+
process.env.VISUALBUILD_APP_DIR || builderPackageRoot
|
|
34
|
+
)
|
|
35
|
+
const builderRoot = path.resolve(builderPackageRoot, 'visual-app-builder')
|
|
36
|
+
const startupProjectConfig = await loadVisualBuildConfig(projectRoot)
|
|
37
|
+
const resolveConfiguredPath = (relativePath: string) =>
|
|
38
|
+
path.resolve(projectRoot, ...relativePath.split('/'))
|
|
39
|
+
const generatedSourceRoot = resolveConfiguredPath(
|
|
40
|
+
startupProjectConfig.paths.sourceDir
|
|
41
|
+
)
|
|
42
|
+
const configuredProjectRoots = [
|
|
43
|
+
generatedSourceRoot,
|
|
44
|
+
...[
|
|
45
|
+
startupProjectConfig.paths.schemaFile,
|
|
46
|
+
startupProjectConfig.paths.componentRegistryFile,
|
|
47
|
+
startupProjectConfig.paths.generatedManifestFile,
|
|
48
|
+
].map((filePath) => path.dirname(resolveConfiguredPath(filePath))),
|
|
49
|
+
]
|
|
50
|
+
const maxEditableFileBytes = 2 * 1024 * 1024
|
|
51
|
+
const hiddenProjectEntries = new Set(['.git', 'node_modules'])
|
|
52
|
+
|
|
53
|
+
function isPathInsideDirectory(filePath: string, directoryPath: string) {
|
|
54
|
+
return (
|
|
55
|
+
filePath === directoryPath ||
|
|
56
|
+
filePath.startsWith(`${directoryPath.replace(/\/+$/, '')}/`)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type ProjectEntryKind = 'file' | 'directory' | 'symlink'
|
|
61
|
+
|
|
62
|
+
interface ProjectEntry {
|
|
63
|
+
name: string
|
|
64
|
+
path: string
|
|
65
|
+
kind: ProjectEntryKind
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ManagedPage {
|
|
69
|
+
id: string
|
|
70
|
+
name: string
|
|
71
|
+
route: string
|
|
72
|
+
sourcePath?: string
|
|
73
|
+
root: ImportedVisualNode
|
|
74
|
+
components: ImportedVisualNode[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ManagedProjectSchema {
|
|
78
|
+
pages: ManagedPage[]
|
|
79
|
+
[key: string]: unknown
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ReverseSyncEvent {
|
|
83
|
+
type: 'reverse-sync-success' | 'reverse-sync-error'
|
|
84
|
+
path: string
|
|
85
|
+
pageId?: string
|
|
86
|
+
message: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface GeneratorDiagnostic {
|
|
90
|
+
severity: 'error'
|
|
91
|
+
phase: 'generate'
|
|
92
|
+
code: string
|
|
93
|
+
title: string
|
|
94
|
+
message: string
|
|
95
|
+
path: string
|
|
96
|
+
suggestion?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface GeneratorDiagnosticEvent extends GeneratorDiagnostic {
|
|
100
|
+
type: 'generator-diagnostic'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface FileSystemEvent {
|
|
104
|
+
type: 'filesystem-change'
|
|
105
|
+
path: string
|
|
106
|
+
action: 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface ManagedPagesDeletedEvent {
|
|
110
|
+
type: 'managed-pages-deleted'
|
|
111
|
+
path: string
|
|
112
|
+
pageIds: string[]
|
|
113
|
+
message: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface ManagedPagePathChange {
|
|
117
|
+
pageId: string
|
|
118
|
+
previousPath: string
|
|
119
|
+
sourcePath: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface ManagedPagesMovedEvent {
|
|
123
|
+
type: 'managed-pages-moved'
|
|
124
|
+
path: string
|
|
125
|
+
targetPath: string
|
|
126
|
+
pages: ManagedPagePathChange[]
|
|
127
|
+
message: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type VisualBuildEvent =
|
|
131
|
+
| ReverseSyncEvent
|
|
132
|
+
| GeneratorDiagnosticEvent
|
|
133
|
+
| FileSystemEvent
|
|
134
|
+
| ManagedPagesDeletedEvent
|
|
135
|
+
| ManagedPagesMovedEvent
|
|
136
|
+
|
|
137
|
+
function isGeneratedProjectPath(watchedPath: string) {
|
|
138
|
+
const absolutePath = path.resolve(watchedPath)
|
|
139
|
+
|
|
140
|
+
return configuredProjectRoots.some(
|
|
141
|
+
(rootPath) =>
|
|
142
|
+
absolutePath === rootPath || absolutePath.startsWith(`${rootPath}${path.sep}`)
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sendJson(
|
|
147
|
+
res: { statusCode: number; setHeader: (name: string, value: string) => void; end: (body?: string) => void },
|
|
148
|
+
statusCode: number,
|
|
149
|
+
payload: unknown
|
|
150
|
+
) {
|
|
151
|
+
res.statusCode = statusCode
|
|
152
|
+
res.setHeader('Content-Type', 'application/json')
|
|
153
|
+
res.end(JSON.stringify(payload))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getProjectPath(relativePath = '') {
|
|
157
|
+
const normalizedInput = relativePath.replace(/\\/g, '/').trim()
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
normalizedInput.startsWith('/') ||
|
|
161
|
+
/^[a-zA-Z]:/.test(normalizedInput) ||
|
|
162
|
+
normalizedInput.includes('\0')
|
|
163
|
+
) {
|
|
164
|
+
throw new Error('Invalid project path')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const segments = normalizedInput
|
|
168
|
+
.split('/')
|
|
169
|
+
.filter((segment) => segment && segment !== '.')
|
|
170
|
+
|
|
171
|
+
if (segments.some((segment) => segment === '..')) {
|
|
172
|
+
throw new Error('Project path cannot leave the app root')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const absolutePath = path.resolve(projectRoot, ...segments)
|
|
176
|
+
const relativeToRoot = path.relative(projectRoot, absolutePath)
|
|
177
|
+
|
|
178
|
+
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
|
|
179
|
+
throw new Error('Project path cannot leave the app root')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
absolutePath,
|
|
184
|
+
relativePath: segments.join('/'),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function assertRealPathInsideProject(absolutePath: string) {
|
|
189
|
+
const [realProjectRoot, realTarget] = await Promise.all([
|
|
190
|
+
fs.realpath(projectRoot),
|
|
191
|
+
fs.realpath(absolutePath),
|
|
192
|
+
])
|
|
193
|
+
const relativeToRoot = path.relative(realProjectRoot, realTarget)
|
|
194
|
+
|
|
195
|
+
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
|
|
196
|
+
throw new Error('Project path cannot leave the app root')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validateEntryName(name: unknown) {
|
|
201
|
+
if (
|
|
202
|
+
typeof name !== 'string' ||
|
|
203
|
+
!name.trim() ||
|
|
204
|
+
name === '.' ||
|
|
205
|
+
name === '..' ||
|
|
206
|
+
/[\\/:*?"<>|]/.test(name)
|
|
207
|
+
) {
|
|
208
|
+
throw new Error('Enter a valid file or folder name')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return name.trim()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function readJsonBody(req: NodeJS.ReadableStream) {
|
|
215
|
+
let body = ''
|
|
216
|
+
|
|
217
|
+
for await (const chunk of req) {
|
|
218
|
+
body += String(chunk)
|
|
219
|
+
|
|
220
|
+
if (body.length > maxEditableFileBytes * 2) {
|
|
221
|
+
throw new Error('Request is too large')
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return body ? (JSON.parse(body) as Record<string, unknown>) : {}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function listProjectDirectory(relativePath: string) {
|
|
229
|
+
const projectPath = getProjectPath(relativePath)
|
|
230
|
+
const stats = await fs.lstat(projectPath.absolutePath)
|
|
231
|
+
|
|
232
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
233
|
+
throw new Error('The selected path is not a directory')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await assertRealPathInsideProject(projectPath.absolutePath)
|
|
237
|
+
const directoryEntries = await fs.readdir(projectPath.absolutePath, {
|
|
238
|
+
withFileTypes: true,
|
|
239
|
+
})
|
|
240
|
+
const entries: ProjectEntry[] = directoryEntries
|
|
241
|
+
.filter((entry) => !hiddenProjectEntries.has(entry.name))
|
|
242
|
+
.map((entry) => {
|
|
243
|
+
const entryPath = projectPath.relativePath
|
|
244
|
+
? `${projectPath.relativePath}/${entry.name}`
|
|
245
|
+
: entry.name
|
|
246
|
+
const kind: ProjectEntryKind = entry.isSymbolicLink()
|
|
247
|
+
? 'symlink'
|
|
248
|
+
: entry.isDirectory()
|
|
249
|
+
? 'directory'
|
|
250
|
+
: 'file'
|
|
251
|
+
|
|
252
|
+
return { name: entry.name, path: entryPath, kind }
|
|
253
|
+
})
|
|
254
|
+
.sort((first, second) => {
|
|
255
|
+
if (first.kind === 'directory' && second.kind !== 'directory') return -1
|
|
256
|
+
if (first.kind !== 'directory' && second.kind === 'directory') return 1
|
|
257
|
+
return first.name.localeCompare(second.name, undefined, {
|
|
258
|
+
numeric: true,
|
|
259
|
+
sensitivity: 'base',
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
rootName: path.basename(projectRoot),
|
|
265
|
+
path: projectPath.relativePath,
|
|
266
|
+
entries,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function readProjectFile(relativePath: string) {
|
|
271
|
+
const projectPath = getProjectPath(relativePath)
|
|
272
|
+
const stats = await fs.lstat(projectPath.absolutePath)
|
|
273
|
+
|
|
274
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
275
|
+
throw new Error('The selected path is not an editable file')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await assertRealPathInsideProject(projectPath.absolutePath)
|
|
279
|
+
|
|
280
|
+
if (stats.size > maxEditableFileBytes) {
|
|
281
|
+
return {
|
|
282
|
+
path: projectPath.relativePath,
|
|
283
|
+
content: '',
|
|
284
|
+
size: stats.size,
|
|
285
|
+
isBinary: false,
|
|
286
|
+
isTooLarge: true,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const buffer = await fs.readFile(projectPath.absolutePath)
|
|
291
|
+
const isBinary = buffer.subarray(0, 8000).includes(0)
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
path: projectPath.relativePath,
|
|
295
|
+
content: isBinary ? '' : buffer.toString('utf8'),
|
|
296
|
+
size: stats.size,
|
|
297
|
+
isBinary,
|
|
298
|
+
isTooLarge: false,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function importPageFromSource(
|
|
303
|
+
relativePath: string,
|
|
304
|
+
name: string,
|
|
305
|
+
route: string
|
|
306
|
+
) {
|
|
307
|
+
const projectPath = getProjectPath(relativePath)
|
|
308
|
+
const config = await loadVisualBuildConfig(projectRoot)
|
|
309
|
+
|
|
310
|
+
if (
|
|
311
|
+
!isPathInsideDirectory(
|
|
312
|
+
projectPath.relativePath,
|
|
313
|
+
config.paths.sourceDir
|
|
314
|
+
) ||
|
|
315
|
+
!/\.(jsx|tsx)$/i.test(projectPath.relativePath)
|
|
316
|
+
) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Pages must use a .jsx or .tsx file inside ${config.paths.sourceDir}`
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const stats = await fs.lstat(projectPath.absolutePath)
|
|
323
|
+
|
|
324
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
325
|
+
throw new Error('Select a valid React source file')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
await assertRealPathInsideProject(projectPath.absolutePath)
|
|
329
|
+
const source = await fs.readFile(projectPath.absolutePath, 'utf8')
|
|
330
|
+
const pageId = randomUUID()
|
|
331
|
+
const converted = parseReactPageSource(
|
|
332
|
+
source,
|
|
333
|
+
projectPath.relativePath,
|
|
334
|
+
(config.components ?? []).map((component) => component.name)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
id: pageId,
|
|
339
|
+
name,
|
|
340
|
+
route,
|
|
341
|
+
sourcePath: projectPath.relativePath,
|
|
342
|
+
root: {
|
|
343
|
+
...converted.root,
|
|
344
|
+
id: `${pageId}:root`,
|
|
345
|
+
label: 'Page root',
|
|
346
|
+
children: [],
|
|
347
|
+
},
|
|
348
|
+
components: converted.components,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function registerComponentFromSource(relativePath: string) {
|
|
353
|
+
const projectPath = getProjectPath(relativePath)
|
|
354
|
+
const currentConfig = await loadVisualBuildConfig(projectRoot)
|
|
355
|
+
|
|
356
|
+
if (
|
|
357
|
+
!isPathInsideDirectory(
|
|
358
|
+
projectPath.relativePath,
|
|
359
|
+
currentConfig.paths.sourceDir
|
|
360
|
+
) ||
|
|
361
|
+
!/\.(jsx|tsx)$/i.test(projectPath.relativePath)
|
|
362
|
+
) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Components must use a .jsx or .tsx file inside ${currentConfig.paths.sourceDir}`
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const stats = await fs.lstat(projectPath.absolutePath)
|
|
369
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
370
|
+
throw new Error('Select a valid React component source file')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await assertRealPathInsideProject(projectPath.absolutePath)
|
|
374
|
+
const source = await fs.readFile(projectPath.absolutePath, 'utf8')
|
|
375
|
+
const exportInfo = findReactComponentExport(
|
|
376
|
+
source,
|
|
377
|
+
projectPath.relativePath
|
|
378
|
+
)
|
|
379
|
+
const registrationsPath = resolveConfiguredPath(
|
|
380
|
+
currentConfig.paths.componentRegistryFile
|
|
381
|
+
)
|
|
382
|
+
const registrations = await fs
|
|
383
|
+
.readFile(registrationsPath, 'utf8')
|
|
384
|
+
.then((contents) => JSON.parse(contents) as { components?: unknown[] })
|
|
385
|
+
.catch((error: NodeJS.ErrnoException) => {
|
|
386
|
+
if (error.code === 'ENOENT') return { components: [] }
|
|
387
|
+
throw error
|
|
388
|
+
})
|
|
389
|
+
if (
|
|
390
|
+
(currentConfig.components ?? []).some(
|
|
391
|
+
(component) => component.name === exportInfo.name
|
|
392
|
+
)
|
|
393
|
+
) {
|
|
394
|
+
throw new Error(`Component "${exportInfo.name}" is already registered`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const registration = {
|
|
398
|
+
name: exportInfo.name,
|
|
399
|
+
importPath: projectPath.relativePath,
|
|
400
|
+
exportName: exportInfo.exportName,
|
|
401
|
+
label: splitComponentName(exportInfo.name),
|
|
402
|
+
category: 'Custom',
|
|
403
|
+
icon: exportInfo.name.charAt(0),
|
|
404
|
+
description: `Local ${exportInfo.name} component`,
|
|
405
|
+
acceptsChildren: true,
|
|
406
|
+
props: {
|
|
407
|
+
wrapperTag: {
|
|
408
|
+
type: 'select',
|
|
409
|
+
label: 'Component root tag',
|
|
410
|
+
default: 'div',
|
|
411
|
+
options: [
|
|
412
|
+
'div',
|
|
413
|
+
'main',
|
|
414
|
+
'section',
|
|
415
|
+
'article',
|
|
416
|
+
'header',
|
|
417
|
+
'footer',
|
|
418
|
+
'nav',
|
|
419
|
+
'aside',
|
|
420
|
+
'form',
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
id: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
label: 'ID',
|
|
426
|
+
default: '',
|
|
427
|
+
},
|
|
428
|
+
className: {
|
|
429
|
+
type: 'text',
|
|
430
|
+
label: 'Tailwind classes',
|
|
431
|
+
default: '',
|
|
432
|
+
},
|
|
433
|
+
attributes: {
|
|
434
|
+
type: 'attributes',
|
|
435
|
+
label: 'Additional attributes',
|
|
436
|
+
default: {},
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
const nextRegistrations = {
|
|
441
|
+
components: [...(registrations.components ?? []), registration],
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await fs.mkdir(path.dirname(registrationsPath), { recursive: true })
|
|
445
|
+
await fs.writeFile(
|
|
446
|
+
registrationsPath,
|
|
447
|
+
`${JSON.stringify(nextRegistrations, null, 2)}\n`,
|
|
448
|
+
'utf8'
|
|
449
|
+
)
|
|
450
|
+
const preview = createComponentPreview(
|
|
451
|
+
source,
|
|
452
|
+
projectPath.relativePath,
|
|
453
|
+
[
|
|
454
|
+
...(currentConfig.components ?? []).map(
|
|
455
|
+
(component) => component.name
|
|
456
|
+
),
|
|
457
|
+
exportInfo.name,
|
|
458
|
+
]
|
|
459
|
+
)
|
|
460
|
+
const previewDefaults = getPreviewRootDefaults(preview)
|
|
461
|
+
return {
|
|
462
|
+
...registration,
|
|
463
|
+
defaultProps: {
|
|
464
|
+
wrapperTag: previewDefaults.wrapperTag ?? 'div',
|
|
465
|
+
id: previewDefaults.id ?? '',
|
|
466
|
+
className: previewDefaults.className ?? '',
|
|
467
|
+
attributes: previewDefaults.attributes ?? {},
|
|
468
|
+
},
|
|
469
|
+
propSchema: registration.props,
|
|
470
|
+
acceptsChildren: preview?.rendersChildren ?? true,
|
|
471
|
+
...(preview ? { preview } : {}),
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function createComponentPreview(
|
|
476
|
+
source: string,
|
|
477
|
+
sourcePath: string,
|
|
478
|
+
customComponentNames: Iterable<string>
|
|
479
|
+
) {
|
|
480
|
+
try {
|
|
481
|
+
return parseReactComponentPreview(
|
|
482
|
+
source,
|
|
483
|
+
sourcePath,
|
|
484
|
+
customComponentNames
|
|
485
|
+
)
|
|
486
|
+
} catch {
|
|
487
|
+
return undefined
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function getPreviewRootDefaults(
|
|
492
|
+
preview: ReturnType<typeof createComponentPreview>
|
|
493
|
+
) {
|
|
494
|
+
if (!preview) return {}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
wrapperTag: preview.rootTag,
|
|
498
|
+
id: preview.rootProps.id ?? '',
|
|
499
|
+
className: preview.rootProps.className ?? '',
|
|
500
|
+
attributes: Object.fromEntries(
|
|
501
|
+
Object.entries(preview.rootProps).filter(
|
|
502
|
+
([name]) => !['id', 'className', 'text'].includes(name)
|
|
503
|
+
)
|
|
504
|
+
),
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function addComponentPreviews(
|
|
509
|
+
components: Awaited<
|
|
510
|
+
ReturnType<typeof loadVisualBuildConfig>
|
|
511
|
+
>['components']
|
|
512
|
+
) {
|
|
513
|
+
const componentNames = components.map((component) => component.name)
|
|
514
|
+
|
|
515
|
+
return Promise.all(
|
|
516
|
+
components.map(async (component) => {
|
|
517
|
+
const source = await fs
|
|
518
|
+
.readFile(getProjectPath(component.importPath).absolutePath, 'utf8')
|
|
519
|
+
.catch(() => null)
|
|
520
|
+
const preview = source
|
|
521
|
+
? createComponentPreview(
|
|
522
|
+
source,
|
|
523
|
+
component.importPath,
|
|
524
|
+
componentNames
|
|
525
|
+
)
|
|
526
|
+
: undefined
|
|
527
|
+
const previewDefaults = getPreviewRootDefaults(preview)
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
...component,
|
|
531
|
+
defaultProps: {
|
|
532
|
+
...component.defaultProps,
|
|
533
|
+
...previewDefaults,
|
|
534
|
+
},
|
|
535
|
+
acceptsChildren:
|
|
536
|
+
preview?.rendersChildren ?? component.acceptsChildren,
|
|
537
|
+
...(preview ? { preview } : {}),
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function findReactComponentExport(source: string, sourcePath: string) {
|
|
544
|
+
const ast = parse(source, {
|
|
545
|
+
sourceType: 'module',
|
|
546
|
+
plugins: ['jsx', 'typescript'],
|
|
547
|
+
})
|
|
548
|
+
const fallbackName = toPascalComponentName(
|
|
549
|
+
path.posix.basename(sourcePath).replace(/\.[^.]+$/, '')
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
for (const statement of ast.program.body) {
|
|
553
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
554
|
+
return {
|
|
555
|
+
name:
|
|
556
|
+
'id' in statement.declaration && statement.declaration.id?.name
|
|
557
|
+
? statement.declaration.id.name
|
|
558
|
+
: fallbackName,
|
|
559
|
+
exportName: 'default',
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
for (const statement of ast.program.body) {
|
|
565
|
+
if (
|
|
566
|
+
statement.type === 'ExportNamedDeclaration' &&
|
|
567
|
+
statement.declaration &&
|
|
568
|
+
'id' in statement.declaration &&
|
|
569
|
+
statement.declaration.id?.type === 'Identifier'
|
|
570
|
+
) {
|
|
571
|
+
return {
|
|
572
|
+
name: statement.declaration.id.name,
|
|
573
|
+
exportName: statement.declaration.id.name,
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
throw new Error(
|
|
579
|
+
'The selected file must export a React component as default or as a named function/class'
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function toPascalComponentName(value: string) {
|
|
584
|
+
const name = value
|
|
585
|
+
.split(/[^A-Za-z0-9]+/)
|
|
586
|
+
.filter(Boolean)
|
|
587
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
588
|
+
.join('')
|
|
589
|
+
return /^[A-Z]/.test(name) ? name : `Component${name}`
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function splitComponentName(value: string) {
|
|
593
|
+
return value.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function getErrorStatus(error: unknown) {
|
|
597
|
+
const code =
|
|
598
|
+
typeof error === 'object' && error !== null && 'code' in error
|
|
599
|
+
? String(error.code)
|
|
600
|
+
: ''
|
|
601
|
+
|
|
602
|
+
if (code === 'ENOENT') return 404
|
|
603
|
+
if (code === 'EEXIST' || code === 'ENOTEMPTY') return 409
|
|
604
|
+
return 400
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const generatorDiagnosticMarker = 'VISUALBUILD_DIAGNOSTIC:'
|
|
608
|
+
|
|
609
|
+
function getProcessOutput(error: unknown, key: 'stdout' | 'stderr') {
|
|
610
|
+
if (
|
|
611
|
+
typeof error !== 'object' ||
|
|
612
|
+
error === null ||
|
|
613
|
+
!(key in error)
|
|
614
|
+
) {
|
|
615
|
+
return ''
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return String(error[key] ?? '')
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function parseGeneratorDiagnostic(
|
|
622
|
+
error: unknown,
|
|
623
|
+
reason: string
|
|
624
|
+
): GeneratorDiagnostic {
|
|
625
|
+
const stderr = getProcessOutput(error, 'stderr')
|
|
626
|
+
const diagnosticLine = stderr
|
|
627
|
+
.split(/\r?\n/)
|
|
628
|
+
.find((line) => line.startsWith(generatorDiagnosticMarker))
|
|
629
|
+
|
|
630
|
+
if (diagnosticLine) {
|
|
631
|
+
try {
|
|
632
|
+
const diagnostic = JSON.parse(
|
|
633
|
+
diagnosticLine.slice(generatorDiagnosticMarker.length)
|
|
634
|
+
) as Partial<GeneratorDiagnostic>
|
|
635
|
+
|
|
636
|
+
if (
|
|
637
|
+
diagnostic.severity === 'error' &&
|
|
638
|
+
diagnostic.phase === 'generate' &&
|
|
639
|
+
typeof diagnostic.code === 'string' &&
|
|
640
|
+
typeof diagnostic.title === 'string' &&
|
|
641
|
+
typeof diagnostic.message === 'string' &&
|
|
642
|
+
typeof diagnostic.path === 'string'
|
|
643
|
+
) {
|
|
644
|
+
return {
|
|
645
|
+
severity: diagnostic.severity,
|
|
646
|
+
phase: diagnostic.phase,
|
|
647
|
+
code: diagnostic.code,
|
|
648
|
+
title: diagnostic.title,
|
|
649
|
+
message: diagnostic.message,
|
|
650
|
+
path: diagnostic.path,
|
|
651
|
+
suggestion:
|
|
652
|
+
typeof diagnostic.suggestion === 'string'
|
|
653
|
+
? diagnostic.suggestion
|
|
654
|
+
: undefined,
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
// Fall through to a stable diagnostic if the child process output is malformed.
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
severity: 'error',
|
|
664
|
+
phase: 'generate',
|
|
665
|
+
code: 'GENERATION_FAILED',
|
|
666
|
+
title: 'React source generation failed',
|
|
667
|
+
message:
|
|
668
|
+
error instanceof Error
|
|
669
|
+
? error.message
|
|
670
|
+
: `Failed to generate project files after ${reason}.`,
|
|
671
|
+
path: startupProjectConfig.paths.schemaFile,
|
|
672
|
+
suggestion:
|
|
673
|
+
'Review the project schema and try again. The last valid files were kept.',
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function getGeneratorDiagnostic(error: unknown) {
|
|
678
|
+
if (
|
|
679
|
+
typeof error !== 'object' ||
|
|
680
|
+
error === null ||
|
|
681
|
+
!('diagnostic' in error)
|
|
682
|
+
) {
|
|
683
|
+
return null
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return error.diagnostic as GeneratorDiagnostic
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function visualbuildDevApi(): Plugin {
|
|
690
|
+
const pagesPath = resolveConfiguredPath(
|
|
691
|
+
startupProjectConfig.paths.schemaFile
|
|
692
|
+
)
|
|
693
|
+
const runtimeClassesPath = path.resolve(
|
|
694
|
+
builderRoot,
|
|
695
|
+
'.visualbuild-runtime-classes.html'
|
|
696
|
+
)
|
|
697
|
+
const generatorPath = path.resolve(
|
|
698
|
+
builderPackageRoot,
|
|
699
|
+
'scripts/vb-generate.mjs'
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
name: 'visualbuild-dev-api',
|
|
704
|
+
async configResolved() {
|
|
705
|
+
await fs.writeFile(
|
|
706
|
+
runtimeClassesPath,
|
|
707
|
+
await fs.readFile(runtimeClassesPath, 'utf8').catch(() => '')
|
|
708
|
+
)
|
|
709
|
+
},
|
|
710
|
+
configureServer(server) {
|
|
711
|
+
let suppressWatchUntil = 0
|
|
712
|
+
let suppressReloadUntil = 0
|
|
713
|
+
let generateTimer: ReturnType<typeof setTimeout> | null = null
|
|
714
|
+
let managedDeletionQueue = Promise.resolve()
|
|
715
|
+
let deploymentInProgress = false
|
|
716
|
+
const reverseSyncTimers = new Map<
|
|
717
|
+
string,
|
|
718
|
+
ReturnType<typeof setTimeout>
|
|
719
|
+
>()
|
|
720
|
+
const suppressedUnlinkPaths = new Map<string, number>()
|
|
721
|
+
const eventClients = new Set<ServerResponse>()
|
|
722
|
+
const hotChannel = server.ws as unknown as {
|
|
723
|
+
send: (payload: unknown, data?: unknown) => void
|
|
724
|
+
}
|
|
725
|
+
const sendHotPayload = hotChannel.send.bind(hotChannel)
|
|
726
|
+
|
|
727
|
+
hotChannel.send = (payload, data) => {
|
|
728
|
+
if (
|
|
729
|
+
Date.now() < suppressReloadUntil &&
|
|
730
|
+
typeof payload === 'object' &&
|
|
731
|
+
payload !== null &&
|
|
732
|
+
'type' in payload &&
|
|
733
|
+
payload.type === 'full-reload'
|
|
734
|
+
) {
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
sendHotPayload(payload, data)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function generateProjectFiles(reason: string) {
|
|
742
|
+
try {
|
|
743
|
+
const { stdout, stderr } = await execFileAsync(
|
|
744
|
+
process.execPath,
|
|
745
|
+
[generatorPath],
|
|
746
|
+
{ cwd: projectRoot }
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
if (stdout.trim()) {
|
|
750
|
+
server.config.logger.info(stdout.trim())
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (stderr.trim()) {
|
|
754
|
+
server.config.logger.warn(stderr.trim())
|
|
755
|
+
}
|
|
756
|
+
} catch (error) {
|
|
757
|
+
const diagnostic = parseGeneratorDiagnostic(error, reason)
|
|
758
|
+
server.config.logger.error(
|
|
759
|
+
`[VisualBuild] ${diagnostic.title}: ${diagnostic.message}`
|
|
760
|
+
)
|
|
761
|
+
broadcastVisualBuildEvent({
|
|
762
|
+
type: 'generator-diagnostic',
|
|
763
|
+
...diagnostic,
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
const generationError = new Error(diagnostic.message)
|
|
767
|
+
Object.assign(generationError, { diagnostic })
|
|
768
|
+
throw generationError
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const pagesWatcher = watchFiles(pagesPath, {
|
|
773
|
+
ignoreInitial: true,
|
|
774
|
+
usePolling: process.platform === 'win32',
|
|
775
|
+
interval: 100,
|
|
776
|
+
awaitWriteFinish: {
|
|
777
|
+
stabilityThreshold: 120,
|
|
778
|
+
pollInterval: 25,
|
|
779
|
+
},
|
|
780
|
+
}).on('change', () => {
|
|
781
|
+
if (Date.now() < suppressWatchUntil) {
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (generateTimer) {
|
|
786
|
+
clearTimeout(generateTimer)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
generateTimer = setTimeout(() => {
|
|
790
|
+
generateTimer = null
|
|
791
|
+
void generateProjectFiles('pages.json change').catch(() => {})
|
|
792
|
+
}, 80)
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
function broadcastVisualBuildEvent(event: VisualBuildEvent) {
|
|
796
|
+
const payload = `event: visualbuild\ndata: ${JSON.stringify(event)}\n\n`
|
|
797
|
+
|
|
798
|
+
for (const client of eventClients) {
|
|
799
|
+
client.write(payload)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function normalizeManagedPath(value: unknown) {
|
|
804
|
+
return typeof value === 'string'
|
|
805
|
+
? value.replace(/\\/g, '/').replace(/^\.\/+/, '')
|
|
806
|
+
: ''
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function findManagedPagesForPath(
|
|
810
|
+
schema: ManagedProjectSchema,
|
|
811
|
+
relativePath: string,
|
|
812
|
+
isDirectory: boolean
|
|
813
|
+
) {
|
|
814
|
+
const normalizedTarget = normalizeManagedPath(relativePath)
|
|
815
|
+
|
|
816
|
+
return schema.pages.filter((page) => {
|
|
817
|
+
const sourcePath = normalizeManagedPath(page.sourcePath)
|
|
818
|
+
|
|
819
|
+
return (
|
|
820
|
+
sourcePath === normalizedTarget ||
|
|
821
|
+
(isDirectory && sourcePath.startsWith(`${normalizedTarget}/`))
|
|
822
|
+
)
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function replaceManagedPathPrefix(
|
|
827
|
+
sourcePath: string,
|
|
828
|
+
previousPath: string,
|
|
829
|
+
targetPath: string
|
|
830
|
+
) {
|
|
831
|
+
return `${targetPath}${sourcePath.slice(previousPath.length)}`
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function validateManagedPagePaths(schema: ManagedProjectSchema) {
|
|
835
|
+
const seenSourcePaths = new Set<string>()
|
|
836
|
+
|
|
837
|
+
for (const page of schema.pages) {
|
|
838
|
+
const sourcePath = normalizeManagedPath(page.sourcePath)
|
|
839
|
+
|
|
840
|
+
if (
|
|
841
|
+
!isPathInsideDirectory(
|
|
842
|
+
sourcePath,
|
|
843
|
+
startupProjectConfig.paths.sourceDir
|
|
844
|
+
) ||
|
|
845
|
+
!/\.(jsx|tsx)$/i.test(sourcePath) ||
|
|
846
|
+
sourcePath.includes('/../') ||
|
|
847
|
+
sourcePath.endsWith('/..')
|
|
848
|
+
) {
|
|
849
|
+
throw new Error(
|
|
850
|
+
`Managed page "${page.name}" must remain a .jsx or .tsx file inside ${startupProjectConfig.paths.sourceDir}`
|
|
851
|
+
)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const layoutPath =
|
|
855
|
+
`${startupProjectConfig.paths.layoutsDir}/AppLayout.tsx`
|
|
856
|
+
if (
|
|
857
|
+
sourcePath === startupProjectConfig.paths.appFile ||
|
|
858
|
+
sourcePath === layoutPath
|
|
859
|
+
) {
|
|
860
|
+
throw new Error(`Managed page path is reserved: ${sourcePath}`)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (seenSourcePaths.has(sourcePath)) {
|
|
864
|
+
throw new Error(`Duplicate managed page path: ${sourcePath}`)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
seenSourcePaths.add(sourcePath)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function suppressManagedUnlink(relativePath: string) {
|
|
872
|
+
suppressedUnlinkPaths.set(
|
|
873
|
+
normalizeManagedPath(relativePath),
|
|
874
|
+
Date.now() + 2500
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function consumeSuppressedUnlink(relativePath: string) {
|
|
879
|
+
const normalizedPath = normalizeManagedPath(relativePath)
|
|
880
|
+
const expiresAt = suppressedUnlinkPaths.get(normalizedPath)
|
|
881
|
+
|
|
882
|
+
if (!expiresAt) return false
|
|
883
|
+
|
|
884
|
+
suppressedUnlinkPaths.delete(normalizedPath)
|
|
885
|
+
return expiresAt >= Date.now()
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async function persistManagedPageDeletion(
|
|
889
|
+
relativePath: string,
|
|
890
|
+
isDirectory: boolean,
|
|
891
|
+
deleteTarget?: () => Promise<void>
|
|
892
|
+
) {
|
|
893
|
+
const schema = JSON.parse(
|
|
894
|
+
await fs.readFile(pagesPath, 'utf8')
|
|
895
|
+
) as ManagedProjectSchema
|
|
896
|
+
const removedPages = findManagedPagesForPath(
|
|
897
|
+
schema,
|
|
898
|
+
relativePath,
|
|
899
|
+
isDirectory
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
if (removedPages.length === 0) {
|
|
903
|
+
if (deleteTarget) await deleteTarget()
|
|
904
|
+
return []
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (removedPages.length === schema.pages.length) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
'A VisualBuild project must keep at least one registered page'
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const removedPageIds = new Set(removedPages.map((page) => page.id))
|
|
914
|
+
const nextSchema: ManagedProjectSchema = {
|
|
915
|
+
...schema,
|
|
916
|
+
pages: schema.pages.filter((page) => !removedPageIds.has(page.id)),
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
for (const page of removedPages) {
|
|
920
|
+
const sourcePath = normalizeManagedPath(page.sourcePath)
|
|
921
|
+
|
|
922
|
+
if (sourcePath) {
|
|
923
|
+
suppressManagedUnlink(sourcePath)
|
|
924
|
+
const pendingTimer = reverseSyncTimers.get(
|
|
925
|
+
path.resolve(projectRoot, sourcePath)
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
if (pendingTimer) {
|
|
929
|
+
clearTimeout(pendingTimer)
|
|
930
|
+
reverseSyncTimers.delete(path.resolve(projectRoot, sourcePath))
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
suppressWatchUntil = Date.now() + 1500
|
|
936
|
+
await fs.writeFile(
|
|
937
|
+
pagesPath,
|
|
938
|
+
`${JSON.stringify(nextSchema, null, 2)}\n`,
|
|
939
|
+
'utf8'
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
if (deleteTarget) await deleteTarget()
|
|
944
|
+
} catch (error) {
|
|
945
|
+
suppressWatchUntil = Date.now() + 1500
|
|
946
|
+
await fs.writeFile(
|
|
947
|
+
pagesPath,
|
|
948
|
+
`${JSON.stringify(schema, null, 2)}\n`,
|
|
949
|
+
'utf8'
|
|
950
|
+
)
|
|
951
|
+
throw error
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
await generateProjectFiles('managed page deletion')
|
|
955
|
+
broadcastVisualBuildEvent({
|
|
956
|
+
type: 'managed-pages-deleted',
|
|
957
|
+
path: normalizeManagedPath(relativePath),
|
|
958
|
+
pageIds: removedPages.map((page) => page.id),
|
|
959
|
+
message:
|
|
960
|
+
removedPages.length === 1
|
|
961
|
+
? `Deleted ${removedPages[0].name}`
|
|
962
|
+
: `Deleted ${removedPages.length} pages`,
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
return removedPages
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function persistManagedPageMove(
|
|
969
|
+
relativePath: string,
|
|
970
|
+
targetRelativePath: string,
|
|
971
|
+
isDirectory: boolean,
|
|
972
|
+
moveTarget: () => Promise<void>,
|
|
973
|
+
rollbackTarget: () => Promise<void>
|
|
974
|
+
) {
|
|
975
|
+
const normalizedSource = normalizeManagedPath(relativePath)
|
|
976
|
+
const normalizedTarget = normalizeManagedPath(targetRelativePath)
|
|
977
|
+
const schema = JSON.parse(
|
|
978
|
+
await fs.readFile(pagesPath, 'utf8')
|
|
979
|
+
) as ManagedProjectSchema
|
|
980
|
+
const affectedPages = findManagedPagesForPath(
|
|
981
|
+
schema,
|
|
982
|
+
normalizedSource,
|
|
983
|
+
isDirectory
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
if (affectedPages.length === 0) {
|
|
987
|
+
await moveTarget()
|
|
988
|
+
return []
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const affectedPageIds = new Set(affectedPages.map((page) => page.id))
|
|
992
|
+
const pathChanges: ManagedPagePathChange[] = affectedPages.map(
|
|
993
|
+
(page) => {
|
|
994
|
+
const previousPath = normalizeManagedPath(page.sourcePath)
|
|
995
|
+
|
|
996
|
+
return {
|
|
997
|
+
pageId: page.id,
|
|
998
|
+
previousPath,
|
|
999
|
+
sourcePath: isDirectory
|
|
1000
|
+
? replaceManagedPathPrefix(
|
|
1001
|
+
previousPath,
|
|
1002
|
+
normalizedSource,
|
|
1003
|
+
normalizedTarget
|
|
1004
|
+
)
|
|
1005
|
+
: normalizedTarget,
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
)
|
|
1009
|
+
const nextPaths = new Map(
|
|
1010
|
+
pathChanges.map((change) => [change.pageId, change.sourcePath])
|
|
1011
|
+
)
|
|
1012
|
+
const nextSchema: ManagedProjectSchema = {
|
|
1013
|
+
...schema,
|
|
1014
|
+
pages: schema.pages.map((page) =>
|
|
1015
|
+
affectedPageIds.has(page.id)
|
|
1016
|
+
? { ...page, sourcePath: nextPaths.get(page.id) }
|
|
1017
|
+
: page
|
|
1018
|
+
),
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
validateManagedPagePaths(nextSchema)
|
|
1022
|
+
|
|
1023
|
+
for (const change of pathChanges) {
|
|
1024
|
+
suppressManagedUnlink(change.previousPath)
|
|
1025
|
+
const previousAbsolutePath = path.resolve(
|
|
1026
|
+
projectRoot,
|
|
1027
|
+
change.previousPath
|
|
1028
|
+
)
|
|
1029
|
+
const pendingTimer = reverseSyncTimers.get(previousAbsolutePath)
|
|
1030
|
+
|
|
1031
|
+
if (pendingTimer) {
|
|
1032
|
+
clearTimeout(pendingTimer)
|
|
1033
|
+
reverseSyncTimers.delete(previousAbsolutePath)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
let targetMoved = false
|
|
1038
|
+
let schemaUpdated = false
|
|
1039
|
+
|
|
1040
|
+
try {
|
|
1041
|
+
await moveTarget()
|
|
1042
|
+
targetMoved = true
|
|
1043
|
+
suppressWatchUntil = Date.now() + 1500
|
|
1044
|
+
await fs.writeFile(
|
|
1045
|
+
pagesPath,
|
|
1046
|
+
`${JSON.stringify(nextSchema, null, 2)}\n`,
|
|
1047
|
+
'utf8'
|
|
1048
|
+
)
|
|
1049
|
+
schemaUpdated = true
|
|
1050
|
+
await generateProjectFiles('managed page move')
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
suppressWatchUntil = Date.now() + 1500
|
|
1053
|
+
|
|
1054
|
+
if (schemaUpdated) {
|
|
1055
|
+
await fs.writeFile(
|
|
1056
|
+
pagesPath,
|
|
1057
|
+
`${JSON.stringify(schema, null, 2)}\n`,
|
|
1058
|
+
'utf8'
|
|
1059
|
+
)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (targetMoved) {
|
|
1063
|
+
await rollbackTarget()
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (schemaUpdated) {
|
|
1067
|
+
await generateProjectFiles('managed page move rollback').catch(
|
|
1068
|
+
() => {}
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
throw error
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
broadcastVisualBuildEvent({
|
|
1076
|
+
type: 'managed-pages-moved',
|
|
1077
|
+
path: normalizedSource,
|
|
1078
|
+
targetPath: normalizedTarget,
|
|
1079
|
+
pages: pathChanges,
|
|
1080
|
+
message:
|
|
1081
|
+
pathChanges.length === 1
|
|
1082
|
+
? `Moved ${affectedPages[0].name}`
|
|
1083
|
+
: `Moved ${pathChanges.length} managed pages`,
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
return pathChanges
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function unregisterExternallyDeletedPage(relativePath: string) {
|
|
1090
|
+
managedDeletionQueue = managedDeletionQueue
|
|
1091
|
+
.then(async () => {
|
|
1092
|
+
const schema = JSON.parse(
|
|
1093
|
+
await fs.readFile(pagesPath, 'utf8')
|
|
1094
|
+
) as ManagedProjectSchema
|
|
1095
|
+
const removedPages = findManagedPagesForPath(
|
|
1096
|
+
schema,
|
|
1097
|
+
relativePath,
|
|
1098
|
+
false
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if (removedPages.length === 0) return
|
|
1102
|
+
|
|
1103
|
+
if (removedPages.length === schema.pages.length) {
|
|
1104
|
+
await generateProjectFiles('restore required page')
|
|
1105
|
+
broadcastVisualBuildEvent({
|
|
1106
|
+
type: 'reverse-sync-error',
|
|
1107
|
+
path: relativePath,
|
|
1108
|
+
pageId: removedPages[0].id,
|
|
1109
|
+
message:
|
|
1110
|
+
'The only registered page cannot be deleted. VisualBuild restored its source file.',
|
|
1111
|
+
})
|
|
1112
|
+
return
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
await persistManagedPageDeletion(relativePath, false)
|
|
1116
|
+
})
|
|
1117
|
+
.catch((error) => {
|
|
1118
|
+
const message =
|
|
1119
|
+
error instanceof Error
|
|
1120
|
+
? error.message
|
|
1121
|
+
: 'Could not unregister the deleted page.'
|
|
1122
|
+
broadcastVisualBuildEvent({
|
|
1123
|
+
type: 'reverse-sync-error',
|
|
1124
|
+
path: relativePath,
|
|
1125
|
+
message,
|
|
1126
|
+
})
|
|
1127
|
+
server.config.logger.warn(
|
|
1128
|
+
`[VisualBuild] Page deletion sync skipped ${relativePath}: ${message}`
|
|
1129
|
+
)
|
|
1130
|
+
})
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function reverseSyncSourceFile(absolutePath: string) {
|
|
1134
|
+
const relativePath = path
|
|
1135
|
+
.relative(projectRoot, absolutePath)
|
|
1136
|
+
.split(path.sep)
|
|
1137
|
+
.join('/')
|
|
1138
|
+
|
|
1139
|
+
try {
|
|
1140
|
+
const schema = JSON.parse(
|
|
1141
|
+
await fs.readFile(pagesPath, 'utf8')
|
|
1142
|
+
) as ManagedProjectSchema
|
|
1143
|
+
const pageIndex = schema.pages.findIndex(
|
|
1144
|
+
(page) =>
|
|
1145
|
+
normalizeManagedPath(page.sourcePath) === relativePath
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
if (pageIndex < 0) return
|
|
1149
|
+
|
|
1150
|
+
const page = schema.pages[pageIndex]
|
|
1151
|
+
const source = await fs.readFile(absolutePath, 'utf8')
|
|
1152
|
+
const config = await loadVisualBuildConfig(projectRoot)
|
|
1153
|
+
const customComponentNames = (config.components ?? []).map(
|
|
1154
|
+
(component) => component.name
|
|
1155
|
+
)
|
|
1156
|
+
const imported = parseReactPageSource(
|
|
1157
|
+
source,
|
|
1158
|
+
relativePath,
|
|
1159
|
+
customComponentNames
|
|
1160
|
+
)
|
|
1161
|
+
const current: ImportedPageTree = {
|
|
1162
|
+
root: page.root,
|
|
1163
|
+
components: page.components,
|
|
1164
|
+
}
|
|
1165
|
+
const reconciled = reconcileImportedTree(
|
|
1166
|
+
imported,
|
|
1167
|
+
current,
|
|
1168
|
+
customComponentNames
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
if (visualTreesEqual(reconciled, current)) return
|
|
1172
|
+
|
|
1173
|
+
schema.pages[pageIndex] = {
|
|
1174
|
+
...page,
|
|
1175
|
+
root: reconciled.root,
|
|
1176
|
+
components: reconciled.components,
|
|
1177
|
+
}
|
|
1178
|
+
suppressWatchUntil = Date.now() + 1200
|
|
1179
|
+
await fs.writeFile(
|
|
1180
|
+
pagesPath,
|
|
1181
|
+
`${JSON.stringify(schema, null, 2)}\n`,
|
|
1182
|
+
'utf8'
|
|
1183
|
+
)
|
|
1184
|
+
broadcastVisualBuildEvent({
|
|
1185
|
+
type: 'reverse-sync-success',
|
|
1186
|
+
path: relativePath,
|
|
1187
|
+
pageId: page.id,
|
|
1188
|
+
message: `Updated ${page.name} from ${relativePath}`,
|
|
1189
|
+
})
|
|
1190
|
+
server.config.logger.info(
|
|
1191
|
+
`[VisualBuild] Reverse synced ${relativePath}`
|
|
1192
|
+
)
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
const message =
|
|
1195
|
+
error instanceof Error
|
|
1196
|
+
? error.message
|
|
1197
|
+
: 'Reverse sync could not parse the managed page.'
|
|
1198
|
+
broadcastVisualBuildEvent({
|
|
1199
|
+
type: 'reverse-sync-error',
|
|
1200
|
+
path: relativePath,
|
|
1201
|
+
message,
|
|
1202
|
+
})
|
|
1203
|
+
server.config.logger.warn(
|
|
1204
|
+
`[VisualBuild] Reverse sync skipped ${relativePath}: ${message}`
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function scheduleReverseSync(absolutePath: string) {
|
|
1210
|
+
if (!/\.(jsx|tsx)$/i.test(absolutePath)) return
|
|
1211
|
+
|
|
1212
|
+
const previousTimer = reverseSyncTimers.get(absolutePath)
|
|
1213
|
+
if (previousTimer) clearTimeout(previousTimer)
|
|
1214
|
+
|
|
1215
|
+
reverseSyncTimers.set(
|
|
1216
|
+
absolutePath,
|
|
1217
|
+
setTimeout(() => {
|
|
1218
|
+
reverseSyncTimers.delete(absolutePath)
|
|
1219
|
+
void reverseSyncSourceFile(absolutePath)
|
|
1220
|
+
}, 100)
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function handleProjectFileChange(
|
|
1225
|
+
action: FileSystemEvent['action'],
|
|
1226
|
+
absolutePath: string
|
|
1227
|
+
) {
|
|
1228
|
+
const relativePath = path
|
|
1229
|
+
.relative(projectRoot, absolutePath)
|
|
1230
|
+
.split(path.sep)
|
|
1231
|
+
.join('/')
|
|
1232
|
+
|
|
1233
|
+
if (
|
|
1234
|
+
!relativePath ||
|
|
1235
|
+
relativePath === '..' ||
|
|
1236
|
+
relativePath.startsWith('../')
|
|
1237
|
+
) {
|
|
1238
|
+
return
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
broadcastVisualBuildEvent({
|
|
1242
|
+
type: 'filesystem-change',
|
|
1243
|
+
path: relativePath,
|
|
1244
|
+
action,
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
const relativeToSource = path.relative(generatedSourceRoot, absolutePath)
|
|
1248
|
+
const isInsideSource =
|
|
1249
|
+
relativeToSource &&
|
|
1250
|
+
relativeToSource !== '..' &&
|
|
1251
|
+
!relativeToSource.startsWith(`..${path.sep}`) &&
|
|
1252
|
+
!path.isAbsolute(relativeToSource)
|
|
1253
|
+
|
|
1254
|
+
if (isInsideSource && (action === 'add' || action === 'change')) {
|
|
1255
|
+
scheduleReverseSync(absolutePath)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (
|
|
1259
|
+
isInsideSource &&
|
|
1260
|
+
action === 'unlink' &&
|
|
1261
|
+
!consumeSuppressedUnlink(relativePath)
|
|
1262
|
+
) {
|
|
1263
|
+
unregisterExternallyDeletedPage(relativePath)
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const projectWatcher = watchFiles(projectRoot, {
|
|
1268
|
+
ignoreInitial: true,
|
|
1269
|
+
usePolling: process.platform === 'win32',
|
|
1270
|
+
interval: 100,
|
|
1271
|
+
ignored: (watchedPath) => {
|
|
1272
|
+
const relativePath = path.relative(projectRoot, path.resolve(watchedPath))
|
|
1273
|
+
const segments = relativePath.split(path.sep)
|
|
1274
|
+
|
|
1275
|
+
return segments.some((segment) =>
|
|
1276
|
+
['.git', 'node_modules', 'dist', '.visualbuild-qa'].includes(segment)
|
|
1277
|
+
)
|
|
1278
|
+
},
|
|
1279
|
+
awaitWriteFinish: {
|
|
1280
|
+
stabilityThreshold: 140,
|
|
1281
|
+
pollInterval: 25,
|
|
1282
|
+
},
|
|
1283
|
+
})
|
|
1284
|
+
.on('add', (changedPath) => handleProjectFileChange('add', changedPath))
|
|
1285
|
+
.on('change', (changedPath) =>
|
|
1286
|
+
handleProjectFileChange('change', changedPath)
|
|
1287
|
+
)
|
|
1288
|
+
.on('unlink', (changedPath) =>
|
|
1289
|
+
handleProjectFileChange('unlink', changedPath)
|
|
1290
|
+
)
|
|
1291
|
+
.on('addDir', (changedPath) =>
|
|
1292
|
+
handleProjectFileChange('addDir', changedPath)
|
|
1293
|
+
)
|
|
1294
|
+
.on('unlinkDir', (changedPath) =>
|
|
1295
|
+
handleProjectFileChange('unlinkDir', changedPath)
|
|
1296
|
+
)
|
|
1297
|
+
.on('ready', () => {
|
|
1298
|
+
server.config.logger.info(
|
|
1299
|
+
`[VisualBuild] Watching ${projectRoot} for project and managed source changes`
|
|
1300
|
+
)
|
|
1301
|
+
})
|
|
1302
|
+
.on('error', (error) => {
|
|
1303
|
+
server.config.logger.warn(
|
|
1304
|
+
`[VisualBuild] Source watcher error: ${error.message}`
|
|
1305
|
+
)
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
const keepAliveTimer = setInterval(() => {
|
|
1309
|
+
for (const client of eventClients) {
|
|
1310
|
+
client.write(': keep-alive\n\n')
|
|
1311
|
+
}
|
|
1312
|
+
}, 15000)
|
|
1313
|
+
|
|
1314
|
+
server.httpServer?.once('close', () => {
|
|
1315
|
+
void pagesWatcher.close()
|
|
1316
|
+
void projectWatcher.close()
|
|
1317
|
+
clearInterval(keepAliveTimer)
|
|
1318
|
+
|
|
1319
|
+
for (const client of eventClients) {
|
|
1320
|
+
client.end()
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
for (const timer of reverseSyncTimers.values()) {
|
|
1324
|
+
clearTimeout(timer)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (generateTimer) {
|
|
1328
|
+
clearTimeout(generateTimer)
|
|
1329
|
+
}
|
|
1330
|
+
})
|
|
1331
|
+
|
|
1332
|
+
void generateProjectFiles('dev server start').catch(() => {})
|
|
1333
|
+
|
|
1334
|
+
server.middlewares.use('/__visualbuild/events', (req, res) => {
|
|
1335
|
+
if (req.method !== 'GET') {
|
|
1336
|
+
res.statusCode = 405
|
|
1337
|
+
res.end()
|
|
1338
|
+
return
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
res.statusCode = 200
|
|
1342
|
+
res.setHeader('Content-Type', 'text/event-stream')
|
|
1343
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
|
1344
|
+
res.setHeader('Connection', 'keep-alive')
|
|
1345
|
+
res.write('retry: 1500\n\n')
|
|
1346
|
+
eventClients.add(res)
|
|
1347
|
+
|
|
1348
|
+
req.once('close', () => {
|
|
1349
|
+
eventClients.delete(res)
|
|
1350
|
+
})
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
server.middlewares.use('/__visualbuild/config', async (req, res) => {
|
|
1354
|
+
if (req.method !== 'GET') {
|
|
1355
|
+
res.statusCode = 405
|
|
1356
|
+
res.end()
|
|
1357
|
+
return
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
try {
|
|
1361
|
+
const config = await loadVisualBuildConfig(projectRoot)
|
|
1362
|
+
sendJson(res, 200, {
|
|
1363
|
+
paths: config.paths,
|
|
1364
|
+
editor: config.editor,
|
|
1365
|
+
generator: config.generator,
|
|
1366
|
+
deployment: config.deployment,
|
|
1367
|
+
customComponents: await addComponentPreviews(
|
|
1368
|
+
config.components ?? []
|
|
1369
|
+
),
|
|
1370
|
+
})
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
sendJson(res, 400, {
|
|
1373
|
+
error:
|
|
1374
|
+
error instanceof Error
|
|
1375
|
+
? error.message
|
|
1376
|
+
: 'Failed to load visualbuild.config.ts',
|
|
1377
|
+
})
|
|
1378
|
+
}
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
server.middlewares.use('/__visualbuild/deploy', async (req, res) => {
|
|
1382
|
+
try {
|
|
1383
|
+
const config = await loadVisualBuildConfig(projectRoot)
|
|
1384
|
+
const packageJson = JSON.parse(
|
|
1385
|
+
await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8')
|
|
1386
|
+
) as { name?: unknown }
|
|
1387
|
+
const defaultProjectName = validateVercelProjectName(
|
|
1388
|
+
config.deployment.projectName ?? packageJson.name ?? path.basename(projectRoot)
|
|
1389
|
+
)
|
|
1390
|
+
const configuredTeamId = validateVercelTeamId(
|
|
1391
|
+
config.deployment.teamId ?? process.env.VERCEL_TEAM_ID
|
|
1392
|
+
)
|
|
1393
|
+
const token =
|
|
1394
|
+
process.env.VERCEL_TOKEN ?? process.env.VERCEL_ACCESS_TOKEN ?? ''
|
|
1395
|
+
|
|
1396
|
+
if (req.method === 'GET') {
|
|
1397
|
+
sendJson(res, 200, {
|
|
1398
|
+
provider: 'vercel',
|
|
1399
|
+
tokenConfigured: Boolean(token.trim()),
|
|
1400
|
+
projectName: defaultProjectName,
|
|
1401
|
+
teamId: configuredTeamId ?? '',
|
|
1402
|
+
outputDirectory: config.deployment.outputDirectory,
|
|
1403
|
+
sourcePolicy: 'static-build-output-only',
|
|
1404
|
+
})
|
|
1405
|
+
return
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (req.method !== 'POST') {
|
|
1409
|
+
res.statusCode = 405
|
|
1410
|
+
res.end()
|
|
1411
|
+
return
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if (!token.trim()) {
|
|
1415
|
+
sendJson(res, 401, {
|
|
1416
|
+
error:
|
|
1417
|
+
'Set VERCEL_TOKEN in the visual-builder terminal, restart the builder, and try again.',
|
|
1418
|
+
})
|
|
1419
|
+
return
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (deploymentInProgress) {
|
|
1423
|
+
sendJson(res, 409, {
|
|
1424
|
+
error: 'A Vercel deployment is already in progress',
|
|
1425
|
+
})
|
|
1426
|
+
return
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const payload = await readJsonBody(req)
|
|
1430
|
+
const projectName = validateVercelProjectName(
|
|
1431
|
+
typeof payload.projectName === 'string'
|
|
1432
|
+
? payload.projectName
|
|
1433
|
+
: defaultProjectName
|
|
1434
|
+
)
|
|
1435
|
+
const teamId = validateVercelTeamId(
|
|
1436
|
+
typeof payload.teamId === 'string'
|
|
1437
|
+
? payload.teamId
|
|
1438
|
+
: configuredTeamId
|
|
1439
|
+
)
|
|
1440
|
+
const outputPath = getProjectPath(
|
|
1441
|
+
config.deployment.outputDirectory
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
deploymentInProgress = true
|
|
1445
|
+
|
|
1446
|
+
try {
|
|
1447
|
+
await generateProjectFiles('Vercel deployment')
|
|
1448
|
+
const npmBuildCommand = resolveNpmBuildCommand()
|
|
1449
|
+
const { stdout, stderr } = await execFileAsync(
|
|
1450
|
+
npmBuildCommand.executable,
|
|
1451
|
+
npmBuildCommand.args,
|
|
1452
|
+
{
|
|
1453
|
+
cwd: projectRoot,
|
|
1454
|
+
env: {
|
|
1455
|
+
...process.env,
|
|
1456
|
+
VISUALBUILD_DEPLOYMENT: 'vercel',
|
|
1457
|
+
},
|
|
1458
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1459
|
+
}
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
if (stdout.trim()) server.config.logger.info(stdout.trim())
|
|
1463
|
+
if (stderr.trim()) server.config.logger.warn(stderr.trim())
|
|
1464
|
+
|
|
1465
|
+
const result = await deployStaticOutputToVercel({
|
|
1466
|
+
outputRoot: outputPath.absolutePath,
|
|
1467
|
+
token,
|
|
1468
|
+
projectName,
|
|
1469
|
+
teamId,
|
|
1470
|
+
})
|
|
1471
|
+
|
|
1472
|
+
sendJson(res, 200, result)
|
|
1473
|
+
} finally {
|
|
1474
|
+
deploymentInProgress = false
|
|
1475
|
+
}
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
const message =
|
|
1478
|
+
error instanceof Error ? error.message : 'Vercel deployment failed'
|
|
1479
|
+
server.config.logger.error(`[VisualBuild] Deployment failed: ${message}`)
|
|
1480
|
+
sendJson(res, 400, { error: message })
|
|
1481
|
+
}
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1484
|
+
server.middlewares.use('/__visualbuild/files', async (req, res) => {
|
|
1485
|
+
const requestUrl = new URL(req.url ?? '/', 'http://visualbuild.local')
|
|
1486
|
+
|
|
1487
|
+
try {
|
|
1488
|
+
if (req.method === 'GET') {
|
|
1489
|
+
const result = await listProjectDirectory(
|
|
1490
|
+
requestUrl.searchParams.get('path') ?? ''
|
|
1491
|
+
)
|
|
1492
|
+
sendJson(res, 200, result)
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
if (req.method === 'POST') {
|
|
1497
|
+
const payload = await readJsonBody(req)
|
|
1498
|
+
const parentPath = getProjectPath(
|
|
1499
|
+
typeof payload.parentPath === 'string' ? payload.parentPath : ''
|
|
1500
|
+
)
|
|
1501
|
+
const parentStats = await fs.lstat(parentPath.absolutePath)
|
|
1502
|
+
|
|
1503
|
+
if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) {
|
|
1504
|
+
throw new Error('Select a valid parent folder')
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
await assertRealPathInsideProject(parentPath.absolutePath)
|
|
1508
|
+
const name = validateEntryName(payload.name)
|
|
1509
|
+
const targetPath = getProjectPath(
|
|
1510
|
+
parentPath.relativePath
|
|
1511
|
+
? `${parentPath.relativePath}/${name}`
|
|
1512
|
+
: name
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
if (payload.kind === 'directory') {
|
|
1516
|
+
await fs.mkdir(targetPath.absolutePath)
|
|
1517
|
+
} else if (payload.kind === 'file') {
|
|
1518
|
+
await fs.writeFile(
|
|
1519
|
+
targetPath.absolutePath,
|
|
1520
|
+
createReactComponentSource(name),
|
|
1521
|
+
{
|
|
1522
|
+
encoding: 'utf8',
|
|
1523
|
+
flag: 'wx',
|
|
1524
|
+
}
|
|
1525
|
+
)
|
|
1526
|
+
} else {
|
|
1527
|
+
throw new Error('Entry type must be file or directory')
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
sendJson(res, 201, {
|
|
1531
|
+
name,
|
|
1532
|
+
path: targetPath.relativePath,
|
|
1533
|
+
kind: payload.kind,
|
|
1534
|
+
})
|
|
1535
|
+
return
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
if (req.method === 'PATCH') {
|
|
1539
|
+
const payload = await readJsonBody(req)
|
|
1540
|
+
const sourcePath = getProjectPath(
|
|
1541
|
+
typeof payload.path === 'string' ? payload.path : ''
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
if (!sourcePath.relativePath) {
|
|
1545
|
+
throw new Error('The project root cannot be renamed')
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
const sourceStats = await fs.lstat(sourcePath.absolutePath)
|
|
1549
|
+
|
|
1550
|
+
if (!sourceStats.isSymbolicLink()) {
|
|
1551
|
+
await assertRealPathInsideProject(sourcePath.absolutePath)
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const name = validateEntryName(payload.name)
|
|
1555
|
+
const currentParentPath = path.posix.dirname(
|
|
1556
|
+
sourcePath.relativePath
|
|
1557
|
+
)
|
|
1558
|
+
const requestedParentPath =
|
|
1559
|
+
typeof payload.parentPath === 'string'
|
|
1560
|
+
? getProjectPath(payload.parentPath)
|
|
1561
|
+
: getProjectPath(
|
|
1562
|
+
currentParentPath === '.' ? '' : currentParentPath
|
|
1563
|
+
)
|
|
1564
|
+
const parentStats = await fs.lstat(
|
|
1565
|
+
requestedParentPath.absolutePath
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
if (
|
|
1569
|
+
!parentStats.isDirectory() ||
|
|
1570
|
+
parentStats.isSymbolicLink()
|
|
1571
|
+
) {
|
|
1572
|
+
throw new Error('Select a valid destination folder')
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
await assertRealPathInsideProject(
|
|
1576
|
+
requestedParentPath.absolutePath
|
|
1577
|
+
)
|
|
1578
|
+
const targetPath = getProjectPath(
|
|
1579
|
+
requestedParentPath.relativePath
|
|
1580
|
+
? `${requestedParentPath.relativePath}/${name}`
|
|
1581
|
+
: name
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
if (targetPath.relativePath === sourcePath.relativePath) {
|
|
1585
|
+
sendJson(res, 200, {
|
|
1586
|
+
name,
|
|
1587
|
+
path: targetPath.relativePath,
|
|
1588
|
+
kind: sourceStats.isSymbolicLink()
|
|
1589
|
+
? 'symlink'
|
|
1590
|
+
: sourceStats.isDirectory()
|
|
1591
|
+
? 'directory'
|
|
1592
|
+
: 'file',
|
|
1593
|
+
managedPages: [],
|
|
1594
|
+
})
|
|
1595
|
+
return
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (
|
|
1599
|
+
sourceStats.isDirectory() &&
|
|
1600
|
+
targetPath.relativePath.startsWith(
|
|
1601
|
+
`${sourcePath.relativePath}/`
|
|
1602
|
+
)
|
|
1603
|
+
) {
|
|
1604
|
+
throw new Error('A folder cannot be moved inside itself')
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const targetExists = await fs
|
|
1608
|
+
.lstat(targetPath.absolutePath)
|
|
1609
|
+
.then(() => true)
|
|
1610
|
+
.catch((error: NodeJS.ErrnoException) => {
|
|
1611
|
+
if (error.code === 'ENOENT') return false
|
|
1612
|
+
throw error
|
|
1613
|
+
})
|
|
1614
|
+
|
|
1615
|
+
if (targetExists) {
|
|
1616
|
+
throw new Error('A file or folder with that name already exists')
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const managedPages = await persistManagedPageMove(
|
|
1620
|
+
sourcePath.relativePath,
|
|
1621
|
+
targetPath.relativePath,
|
|
1622
|
+
sourceStats.isDirectory() && !sourceStats.isSymbolicLink(),
|
|
1623
|
+
() =>
|
|
1624
|
+
fs.rename(sourcePath.absolutePath, targetPath.absolutePath),
|
|
1625
|
+
() =>
|
|
1626
|
+
fs.rename(targetPath.absolutePath, sourcePath.absolutePath)
|
|
1627
|
+
)
|
|
1628
|
+
sendJson(res, 200, {
|
|
1629
|
+
name,
|
|
1630
|
+
path: targetPath.relativePath,
|
|
1631
|
+
kind: sourceStats.isSymbolicLink()
|
|
1632
|
+
? 'symlink'
|
|
1633
|
+
: sourceStats.isDirectory()
|
|
1634
|
+
? 'directory'
|
|
1635
|
+
: 'file',
|
|
1636
|
+
managedPages,
|
|
1637
|
+
})
|
|
1638
|
+
return
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (req.method === 'DELETE') {
|
|
1642
|
+
const targetPath = getProjectPath(
|
|
1643
|
+
requestUrl.searchParams.get('path') ?? ''
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
if (!targetPath.relativePath) {
|
|
1647
|
+
throw new Error('The project root cannot be deleted')
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const targetStats = await fs.lstat(targetPath.absolutePath)
|
|
1651
|
+
|
|
1652
|
+
if (!targetStats.isSymbolicLink()) {
|
|
1653
|
+
await assertRealPathInsideProject(targetPath.absolutePath)
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const deleteTarget = () =>
|
|
1657
|
+
fs.rm(targetPath.absolutePath, {
|
|
1658
|
+
recursive: targetStats.isDirectory(),
|
|
1659
|
+
})
|
|
1660
|
+
const removedPages = await persistManagedPageDeletion(
|
|
1661
|
+
targetPath.relativePath,
|
|
1662
|
+
targetStats.isDirectory(),
|
|
1663
|
+
deleteTarget
|
|
1664
|
+
)
|
|
1665
|
+
|
|
1666
|
+
sendJson(res, 200, {
|
|
1667
|
+
path: targetPath.relativePath,
|
|
1668
|
+
removedPageIds: removedPages.map((page) => page.id),
|
|
1669
|
+
})
|
|
1670
|
+
return
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
res.statusCode = 405
|
|
1674
|
+
res.end()
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
sendJson(res, getErrorStatus(error), {
|
|
1677
|
+
error:
|
|
1678
|
+
error instanceof Error
|
|
1679
|
+
? error.message
|
|
1680
|
+
: 'Project file operation failed',
|
|
1681
|
+
})
|
|
1682
|
+
}
|
|
1683
|
+
})
|
|
1684
|
+
|
|
1685
|
+
server.middlewares.use('/__visualbuild/file', async (req, res) => {
|
|
1686
|
+
const requestUrl = new URL(req.url ?? '/', 'http://visualbuild.local')
|
|
1687
|
+
|
|
1688
|
+
try {
|
|
1689
|
+
if (req.method === 'GET') {
|
|
1690
|
+
sendJson(
|
|
1691
|
+
res,
|
|
1692
|
+
200,
|
|
1693
|
+
await readProjectFile(requestUrl.searchParams.get('path') ?? '')
|
|
1694
|
+
)
|
|
1695
|
+
return
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (req.method === 'PUT') {
|
|
1699
|
+
const payload = await readJsonBody(req)
|
|
1700
|
+
const targetPath = getProjectPath(
|
|
1701
|
+
typeof payload.path === 'string' ? payload.path : ''
|
|
1702
|
+
)
|
|
1703
|
+
const stats = await fs.lstat(targetPath.absolutePath)
|
|
1704
|
+
|
|
1705
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
1706
|
+
throw new Error('The selected path is not an editable file')
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
await assertRealPathInsideProject(targetPath.absolutePath)
|
|
1710
|
+
|
|
1711
|
+
if (typeof payload.content !== 'string') {
|
|
1712
|
+
throw new Error('File content must be text')
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
if (Buffer.byteLength(payload.content, 'utf8') > maxEditableFileBytes) {
|
|
1716
|
+
throw new Error('File content exceeds the 2 MB editor limit')
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
await fs.writeFile(targetPath.absolutePath, payload.content, 'utf8')
|
|
1720
|
+
res.statusCode = 204
|
|
1721
|
+
res.end()
|
|
1722
|
+
return
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
res.statusCode = 405
|
|
1726
|
+
res.end()
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
sendJson(res, getErrorStatus(error), {
|
|
1729
|
+
error:
|
|
1730
|
+
error instanceof Error
|
|
1731
|
+
? error.message
|
|
1732
|
+
: 'Project file operation failed',
|
|
1733
|
+
})
|
|
1734
|
+
}
|
|
1735
|
+
})
|
|
1736
|
+
|
|
1737
|
+
server.middlewares.use('/__visualbuild/import-page', async (req, res) => {
|
|
1738
|
+
try {
|
|
1739
|
+
if (req.method !== 'POST') {
|
|
1740
|
+
res.statusCode = 405
|
|
1741
|
+
res.end()
|
|
1742
|
+
return
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const payload = await readJsonBody(req)
|
|
1746
|
+
const name =
|
|
1747
|
+
typeof payload.name === 'string' ? payload.name.trim() : ''
|
|
1748
|
+
const route =
|
|
1749
|
+
typeof payload.route === 'string' ? payload.route.trim() : ''
|
|
1750
|
+
const sourcePath =
|
|
1751
|
+
typeof payload.path === 'string' ? payload.path : ''
|
|
1752
|
+
|
|
1753
|
+
if (!name) {
|
|
1754
|
+
throw new Error('Page name is required')
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (!route.startsWith('/')) {
|
|
1758
|
+
throw new Error('Page route must start with "/"')
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
sendJson(
|
|
1762
|
+
res,
|
|
1763
|
+
201,
|
|
1764
|
+
await importPageFromSource(sourcePath, name, route)
|
|
1765
|
+
)
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
sendJson(res, getErrorStatus(error), {
|
|
1768
|
+
error:
|
|
1769
|
+
error instanceof Error
|
|
1770
|
+
? error.message
|
|
1771
|
+
: 'Failed to import page source',
|
|
1772
|
+
})
|
|
1773
|
+
}
|
|
1774
|
+
})
|
|
1775
|
+
|
|
1776
|
+
server.middlewares.use(
|
|
1777
|
+
'/__visualbuild/register-component',
|
|
1778
|
+
async (req, res) => {
|
|
1779
|
+
try {
|
|
1780
|
+
if (req.method !== 'POST') {
|
|
1781
|
+
res.statusCode = 405
|
|
1782
|
+
res.end()
|
|
1783
|
+
return
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const payload = await readJsonBody(req)
|
|
1787
|
+
const sourcePath =
|
|
1788
|
+
typeof payload.path === 'string' ? payload.path : ''
|
|
1789
|
+
sendJson(
|
|
1790
|
+
res,
|
|
1791
|
+
201,
|
|
1792
|
+
await registerComponentFromSource(sourcePath)
|
|
1793
|
+
)
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
sendJson(res, getErrorStatus(error), {
|
|
1796
|
+
error:
|
|
1797
|
+
error instanceof Error
|
|
1798
|
+
? error.message
|
|
1799
|
+
: 'Failed to register component source',
|
|
1800
|
+
})
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
server.middlewares.use('/__visualbuild/tailwind', (req, res) => {
|
|
1806
|
+
if (req.method !== 'POST') {
|
|
1807
|
+
res.statusCode = 405
|
|
1808
|
+
res.end()
|
|
1809
|
+
return
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
let body = ''
|
|
1813
|
+
|
|
1814
|
+
req.on('data', (chunk) => {
|
|
1815
|
+
body += chunk
|
|
1816
|
+
})
|
|
1817
|
+
|
|
1818
|
+
req.on('end', async () => {
|
|
1819
|
+
try {
|
|
1820
|
+
const current = await fs
|
|
1821
|
+
.readFile(runtimeClassesPath, 'utf8')
|
|
1822
|
+
.catch(() => '')
|
|
1823
|
+
|
|
1824
|
+
if (current !== body) {
|
|
1825
|
+
suppressReloadUntil = Date.now() + 1500
|
|
1826
|
+
await fs.writeFile(runtimeClassesPath, body)
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
res.statusCode = 204
|
|
1830
|
+
res.end()
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
res.statusCode = 500
|
|
1833
|
+
res.end(
|
|
1834
|
+
error instanceof Error
|
|
1835
|
+
? error.message
|
|
1836
|
+
: 'Failed to update Tailwind classes'
|
|
1837
|
+
)
|
|
1838
|
+
}
|
|
1839
|
+
})
|
|
1840
|
+
})
|
|
1841
|
+
|
|
1842
|
+
server.middlewares.use('/__visualbuild/pages', async (req, res) => {
|
|
1843
|
+
if (req.method === 'GET') {
|
|
1844
|
+
try {
|
|
1845
|
+
const json = await fs.readFile(pagesPath, 'utf8')
|
|
1846
|
+
res.setHeader('Content-Type', 'application/json')
|
|
1847
|
+
res.setHeader(
|
|
1848
|
+
'X-VisualBuild-Project-Id',
|
|
1849
|
+
encodeURIComponent(projectRoot)
|
|
1850
|
+
)
|
|
1851
|
+
res.end(json)
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
res.statusCode = 500
|
|
1854
|
+
res.end(
|
|
1855
|
+
error instanceof Error
|
|
1856
|
+
? error.message
|
|
1857
|
+
: 'Failed to read pages.json'
|
|
1858
|
+
)
|
|
1859
|
+
}
|
|
1860
|
+
return
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
if (req.method === 'POST') {
|
|
1864
|
+
let body = ''
|
|
1865
|
+
|
|
1866
|
+
req.on('data', (chunk) => {
|
|
1867
|
+
body += chunk
|
|
1868
|
+
})
|
|
1869
|
+
|
|
1870
|
+
req.on('end', async () => {
|
|
1871
|
+
let previousSchema = ''
|
|
1872
|
+
let wroteSchema = false
|
|
1873
|
+
|
|
1874
|
+
try {
|
|
1875
|
+
await fs.mkdir(path.dirname(pagesPath), { recursive: true })
|
|
1876
|
+
previousSchema = await fs
|
|
1877
|
+
.readFile(pagesPath, 'utf8')
|
|
1878
|
+
.catch(() => '')
|
|
1879
|
+
|
|
1880
|
+
if (previousSchema !== body) {
|
|
1881
|
+
suppressWatchUntil = Date.now() + 1000
|
|
1882
|
+
suppressReloadUntil = Date.now() + 5000
|
|
1883
|
+
await fs.writeFile(pagesPath, body)
|
|
1884
|
+
wroteSchema = true
|
|
1885
|
+
await generateProjectFiles('pages.json write')
|
|
1886
|
+
suppressReloadUntil = Date.now() + 1000
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
res.statusCode = 204
|
|
1890
|
+
res.end()
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
if (wroteSchema) {
|
|
1893
|
+
suppressWatchUntil = Date.now() + 1000
|
|
1894
|
+
|
|
1895
|
+
if (previousSchema) {
|
|
1896
|
+
await fs.writeFile(pagesPath, previousSchema)
|
|
1897
|
+
await generateProjectFiles('pages.json rollback').catch(
|
|
1898
|
+
(rollbackError) => {
|
|
1899
|
+
server.config.logger.error(
|
|
1900
|
+
`[VisualBuild] Failed to restore generated files: ${
|
|
1901
|
+
rollbackError instanceof Error
|
|
1902
|
+
? rollbackError.message
|
|
1903
|
+
: String(rollbackError)
|
|
1904
|
+
}`
|
|
1905
|
+
)
|
|
1906
|
+
}
|
|
1907
|
+
)
|
|
1908
|
+
} else {
|
|
1909
|
+
await fs.rm(pagesPath, { force: true })
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
const diagnostic = getGeneratorDiagnostic(error)
|
|
1914
|
+
sendJson(res, 500, {
|
|
1915
|
+
error:
|
|
1916
|
+
diagnostic?.message ??
|
|
1917
|
+
(error instanceof Error
|
|
1918
|
+
? error.message
|
|
1919
|
+
: 'Failed to write pages.json'),
|
|
1920
|
+
diagnostic,
|
|
1921
|
+
})
|
|
1922
|
+
}
|
|
1923
|
+
})
|
|
1924
|
+
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
res.statusCode = 405
|
|
1929
|
+
res.end()
|
|
1930
|
+
})
|
|
1931
|
+
},
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
export default defineConfig({
|
|
1936
|
+
root: builderRoot,
|
|
1937
|
+
define: {
|
|
1938
|
+
'process.env.BABEL_TYPES_8_BREAKING': 'false',
|
|
1939
|
+
},
|
|
1940
|
+
plugins: [react(), tailwindcss(), visualbuildDevApi()],
|
|
1941
|
+
server: {
|
|
1942
|
+
watch: {
|
|
1943
|
+
ignored: ['**/node_modules/**', isGeneratedProjectPath],
|
|
1944
|
+
},
|
|
1945
|
+
},
|
|
1946
|
+
})
|