@jami-studio/core 0.92.11 → 0.92.13
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/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/workspace-dev.ts +1816 -1557
- package/corpus/core/src/deploy/build.ts +3797 -3754
- package/corpus/core/src/vite/client.ts +1 -0
- package/dist/cli/workspace-dev.d.ts +43 -0
- package/dist/cli/workspace-dev.d.ts.map +1 -1
- package/dist/cli/workspace-dev.js +299 -74
- package/dist/cli/workspace-dev.js.map +1 -1
- package/dist/deploy/build.d.ts +10 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +694 -656
- package/dist/deploy/build.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,1557 +1,1816 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import http from "node:http";
|
|
5
|
-
import net from "node:net";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import type { Duplex } from "node:stream";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
import * as Sentry from "@sentry/node";
|
|
11
|
-
|
|
12
|
-
import { extractOAuthStateAppId } from "../shared/oauth-state.js";
|
|
13
|
-
import {
|
|
14
|
-
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
15
|
-
workspaceAppAudienceFromPackageJson,
|
|
16
|
-
workspaceAppRouteAccessFromPackageJson,
|
|
17
|
-
type WorkspaceAppAudience,
|
|
18
|
-
} from "../shared/workspace-app-audience.js";
|
|
19
|
-
import {
|
|
20
|
-
attachGatewaySocketErrorSink,
|
|
21
|
-
normalizeOrigin,
|
|
22
|
-
rewriteRedirectLocation,
|
|
23
|
-
escapeHtml,
|
|
24
|
-
} from "./gateway-helpers.js";
|
|
25
|
-
|
|
26
|
-
export interface WorkspaceApp {
|
|
27
|
-
id: string;
|
|
28
|
-
name: string;
|
|
29
|
-
description: string;
|
|
30
|
-
audience: WorkspaceAppAudience;
|
|
31
|
-
publicPaths: string[];
|
|
32
|
-
protectedPaths: string[];
|
|
33
|
-
dir: string;
|
|
34
|
-
port: number;
|
|
35
|
-
process?: ChildProcess;
|
|
36
|
-
restartTimer?: NodeJS.Timeout;
|
|
37
|
-
restartAttempts?: number;
|
|
38
|
-
lastFailure?: {
|
|
39
|
-
code: number | null;
|
|
40
|
-
signal: NodeJS.Signals | null;
|
|
41
|
-
at: number;
|
|
42
|
-
installing: boolean;
|
|
43
|
-
output: string;
|
|
44
|
-
nextRetryAt: number;
|
|
45
|
-
};
|
|
46
|
-
outputTail?: string;
|
|
47
|
-
installing?: boolean;
|
|
48
|
-
installAttempted?: boolean;
|
|
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
|
-
env.
|
|
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
|
-
if (
|
|
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
|
-
|
|
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
|
-
return
|
|
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
|
-
return
|
|
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
|
-
const
|
|
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
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
);
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
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
|
-
function
|
|
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
|
-
app
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
);
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
if (!
|
|
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
|
-
|
|
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
|
-
if (!
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
}
|
|
1503
|
-
)
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { Duplex } from "node:stream";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
import * as Sentry from "@sentry/node";
|
|
11
|
+
|
|
12
|
+
import { extractOAuthStateAppId } from "../shared/oauth-state.js";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
15
|
+
workspaceAppAudienceFromPackageJson,
|
|
16
|
+
workspaceAppRouteAccessFromPackageJson,
|
|
17
|
+
type WorkspaceAppAudience,
|
|
18
|
+
} from "../shared/workspace-app-audience.js";
|
|
19
|
+
import {
|
|
20
|
+
attachGatewaySocketErrorSink,
|
|
21
|
+
normalizeOrigin,
|
|
22
|
+
rewriteRedirectLocation,
|
|
23
|
+
escapeHtml,
|
|
24
|
+
} from "./gateway-helpers.js";
|
|
25
|
+
|
|
26
|
+
export interface WorkspaceApp {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
audience: WorkspaceAppAudience;
|
|
31
|
+
publicPaths: string[];
|
|
32
|
+
protectedPaths: string[];
|
|
33
|
+
dir: string;
|
|
34
|
+
port: number;
|
|
35
|
+
process?: ChildProcess;
|
|
36
|
+
restartTimer?: NodeJS.Timeout;
|
|
37
|
+
restartAttempts?: number;
|
|
38
|
+
lastFailure?: {
|
|
39
|
+
code: number | null;
|
|
40
|
+
signal: NodeJS.Signals | null;
|
|
41
|
+
at: number;
|
|
42
|
+
installing: boolean;
|
|
43
|
+
output: string;
|
|
44
|
+
nextRetryAt: number;
|
|
45
|
+
};
|
|
46
|
+
outputTail?: string;
|
|
47
|
+
installing?: boolean;
|
|
48
|
+
installAttempted?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Built-mode lifecycle. `devMode` marks an app that has been promoted to
|
|
51
|
+
* the vite dev server for the rest of the session (source changed, build
|
|
52
|
+
* failed, or no build could be produced). `building` marks a production
|
|
53
|
+
* build in flight as the app's prep child. `buildAttempted` prevents
|
|
54
|
+
* rebuild loops; `buildChecked` caches the once-per-session staleness scan.
|
|
55
|
+
*/
|
|
56
|
+
devMode?: boolean;
|
|
57
|
+
building?: boolean;
|
|
58
|
+
buildAttempted?: boolean;
|
|
59
|
+
buildChecked?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Set before an intentional kill (promotion to dev mode) so the exit
|
|
62
|
+
* handler respawns immediately instead of treating it as a crash.
|
|
63
|
+
*/
|
|
64
|
+
expectedExit?: boolean;
|
|
65
|
+
promoteWatcher?: fs.FSWatcher;
|
|
66
|
+
/**
|
|
67
|
+
* Set true once we've successfully connected to the upstream. After that we
|
|
68
|
+
* skip the readiness probe on every request; the child server stays
|
|
69
|
+
* listening for the rest of the dev session.
|
|
70
|
+
*/
|
|
71
|
+
ready?: boolean;
|
|
72
|
+
readinessProbe?: Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WorkspaceDevOptions {
|
|
76
|
+
args?: string[];
|
|
77
|
+
env?: NodeJS.ProcessEnv;
|
|
78
|
+
root?: string;
|
|
79
|
+
spawnProcess?: typeof spawn;
|
|
80
|
+
openBrowser?: boolean;
|
|
81
|
+
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
82
|
+
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WorkspaceDevHandle {
|
|
86
|
+
apps: WorkspaceApp[];
|
|
87
|
+
defaultApp: string;
|
|
88
|
+
gatewayUrl: () => string;
|
|
89
|
+
ready: Promise<{ port: number; url: string }>;
|
|
90
|
+
server: http.Server;
|
|
91
|
+
shutdown: () => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DEFAULT_GATEWAY_HOST = "127.0.0.1";
|
|
95
|
+
const DEFAULT_GATEWAY_PORT = 8080;
|
|
96
|
+
const DEFAULT_APP_PORT_START = 8100;
|
|
97
|
+
const PROXY_READY_RETRY_DELAY_MS = 250;
|
|
98
|
+
const APP_RESTART_MAX_DELAY_MS = 10_000;
|
|
99
|
+
const DEFAULT_PROXY_RESPONSE_TIMEOUT_MS = 5_000;
|
|
100
|
+
const DEFAULT_PROXY_NON_HTML_RESPONSE_TIMEOUT_MS = 120_000;
|
|
101
|
+
const APP_OUTPUT_TAIL_BYTES = 8_000;
|
|
102
|
+
const POLLING_WATCH_INTERVAL_MS = "1000";
|
|
103
|
+
const reportedDiscoverAppsReadFailures = new Set<string>();
|
|
104
|
+
const STARTING_APP_RESPONSE_HEADERS: http.OutgoingHttpHeaders = {
|
|
105
|
+
"content-type": "text/html; charset=utf-8",
|
|
106
|
+
"cache-control": "no-store, no-cache, max-age=0, must-revalidate",
|
|
107
|
+
pragma: "no-cache",
|
|
108
|
+
expires: "0",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function workspaceOAuthOrigin(
|
|
112
|
+
env: NodeJS.ProcessEnv,
|
|
113
|
+
gatewayUrl: string,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
return (
|
|
116
|
+
normalizeOrigin(env.VITE_WORKSPACE_OAUTH_ORIGIN) ||
|
|
117
|
+
normalizeOrigin(env.WORKSPACE_OAUTH_ORIGIN) ||
|
|
118
|
+
normalizeOrigin(env.APP_URL) ||
|
|
119
|
+
normalizeOrigin(env.BETTER_AUTH_URL) ||
|
|
120
|
+
normalizeOrigin(gatewayUrl)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isWorkspaceWatcherLimitError(
|
|
125
|
+
err: Pick<NodeJS.ErrnoException, "code">,
|
|
126
|
+
): boolean {
|
|
127
|
+
return err.code === "ENOSPC" || err.code === "EMFILE";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function shouldEagerStartWorkspaceApps(
|
|
131
|
+
args: string[] = [],
|
|
132
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
133
|
+
): boolean {
|
|
134
|
+
return (
|
|
135
|
+
args.includes("--eager") ||
|
|
136
|
+
env.WORKSPACE_EAGER === "1" ||
|
|
137
|
+
env.WORKSPACE_EAGER === "true"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Mixed-mode gateway: when enabled (--built / WORKSPACE_BUILT=1), each app
|
|
143
|
+
* boots from its prebuilt production server (`.output/server/index.mjs`,
|
|
144
|
+
* instant, prod-shaped) instead of a vite dev server. The gateway builds an
|
|
145
|
+
* app first when no fresh build exists, and automatically promotes an app to
|
|
146
|
+
* the vite dev server the moment one of its source files changes — so the
|
|
147
|
+
* app being edited gets hot reload while every other app stays on built
|
|
148
|
+
* output. Nothing is manual: editing is the switch.
|
|
149
|
+
*/
|
|
150
|
+
export function shouldRunBuiltWorkspaceApps(
|
|
151
|
+
args: string[] = [],
|
|
152
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
153
|
+
): boolean {
|
|
154
|
+
if (args.includes("--no-built")) return false;
|
|
155
|
+
if (env.WORKSPACE_BUILT === "0" || env.WORKSPACE_BUILT === "false") {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return (
|
|
159
|
+
args.includes("--built") ||
|
|
160
|
+
env.WORKSPACE_BUILT === "1" ||
|
|
161
|
+
env.WORKSPACE_BUILT === "true"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const BUILT_MODE_IGNORED_SEGMENTS = new Set([
|
|
166
|
+
"node_modules",
|
|
167
|
+
"build",
|
|
168
|
+
"dist",
|
|
169
|
+
"data",
|
|
170
|
+
"media",
|
|
171
|
+
"coverage",
|
|
172
|
+
"test-results",
|
|
173
|
+
"playwright-report",
|
|
174
|
+
]);
|
|
175
|
+
const BUILT_MODE_IGNORED_FILE_PATTERN =
|
|
176
|
+
/\.(log|tmp|db|db-wal|db-shm|sqlite|sqlite-wal|sqlite-shm)$/i;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Whether a path relative to an app dir counts as "source" for built-mode
|
|
180
|
+
* staleness checks and dev-server promotion. Excludes build outputs, data,
|
|
181
|
+
* dependency trees, and every dot-directory/dot-file (`.output`,
|
|
182
|
+
* `.deploy-tmp`, `.react-router`, `.env`, ...) — an `.env` edit needs a
|
|
183
|
+
* restart, not a permanent promotion to vite.
|
|
184
|
+
*/
|
|
185
|
+
export function isBuiltModeSourcePath(relPath: string): boolean {
|
|
186
|
+
if (!relPath) return false;
|
|
187
|
+
const segments = relPath.split(/[\\/]/).filter(Boolean);
|
|
188
|
+
if (segments.length === 0) return false;
|
|
189
|
+
for (const segment of segments) {
|
|
190
|
+
if (segment.startsWith(".")) return false;
|
|
191
|
+
if (BUILT_MODE_IGNORED_SEGMENTS.has(segment)) return false;
|
|
192
|
+
}
|
|
193
|
+
return !BUILT_MODE_IGNORED_FILE_PATTERN.test(relPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function builtWorkspaceAppServerEntry(appDir: string): string {
|
|
197
|
+
return path.join(appDir, ".output", "server", "index.mjs");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Newest mtime across an app's source files (build outputs, data, and
|
|
202
|
+
* node_modules excluded). Used once per session per app to decide whether
|
|
203
|
+
* the prebuilt server is stale and needs a rebuild before serving.
|
|
204
|
+
*/
|
|
205
|
+
export function newestBuiltModeSourceMtimeMs(
|
|
206
|
+
dir: string,
|
|
207
|
+
relBase = "",
|
|
208
|
+
): number {
|
|
209
|
+
let entries: fs.Dirent[];
|
|
210
|
+
try {
|
|
211
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
212
|
+
} catch {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
let newest = 0;
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
218
|
+
if (entry.isDirectory()) {
|
|
219
|
+
if (
|
|
220
|
+
entry.name.startsWith(".") ||
|
|
221
|
+
BUILT_MODE_IGNORED_SEGMENTS.has(entry.name)
|
|
222
|
+
) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
newest = Math.max(
|
|
226
|
+
newest,
|
|
227
|
+
newestBuiltModeSourceMtimeMs(path.join(dir, entry.name), rel),
|
|
228
|
+
);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!entry.isFile() || !isBuiltModeSourcePath(rel)) continue;
|
|
232
|
+
try {
|
|
233
|
+
newest = Math.max(
|
|
234
|
+
newest,
|
|
235
|
+
fs.statSync(path.join(dir, entry.name)).mtimeMs,
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
// File vanished mid-scan — ignore.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return newest;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Whether the gateway should spawn the rest of the apps in the background
|
|
246
|
+
* after the default app boots. Lazy spawn already covers correctness — this
|
|
247
|
+
* is purely a UX optimization so the second/third/Nth app the user clicks
|
|
248
|
+
* into is already warm instead of paying the Vite + esbuild prebundle cost
|
|
249
|
+
* on demand.
|
|
250
|
+
*
|
|
251
|
+
* Defaults to ON in lazy mode. Off when the user passed --no-prewarm /
|
|
252
|
+
* WORKSPACE_NO_PREWARM=1, or when eager mode is already starting every app
|
|
253
|
+
* up front (in which case prewarm would be redundant).
|
|
254
|
+
*/
|
|
255
|
+
export function shouldPrewarmWorkspaceApps(
|
|
256
|
+
args: string[] = [],
|
|
257
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (args.includes("--no-prewarm")) return false;
|
|
260
|
+
if (env.WORKSPACE_NO_PREWARM === "1" || env.WORKSPACE_NO_PREWARM === "true") {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
// Eager mode starts every app immediately; prewarm has nothing to do.
|
|
264
|
+
if (shouldEagerStartWorkspaceApps(args, env)) return false;
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* How many apps to prewarm in parallel. Each Vite spawn briefly maxes out a
|
|
270
|
+
* CPU core during esbuild prebundling, so booting all 9 templates at once on
|
|
271
|
+
* a 4-core laptop just produces a thundering herd. Default 2 — gentle on
|
|
272
|
+
* laptops, fast enough that all apps finish within a few cold-spawn windows.
|
|
273
|
+
* Override via --prewarm-concurrency=N or WORKSPACE_PREWARM_CONCURRENCY=N.
|
|
274
|
+
*/
|
|
275
|
+
export function workspacePrewarmConcurrency(
|
|
276
|
+
args: string[] = [],
|
|
277
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
278
|
+
): number {
|
|
279
|
+
const flag = args.find((arg) => arg.startsWith("--prewarm-concurrency="));
|
|
280
|
+
const raw = flag
|
|
281
|
+
? flag.slice("--prewarm-concurrency=".length)
|
|
282
|
+
: env.WORKSPACE_PREWARM_CONCURRENCY;
|
|
283
|
+
if (!raw) return 2;
|
|
284
|
+
const parsed = Number(raw);
|
|
285
|
+
if (!Number.isFinite(parsed) || parsed < 1) return 2;
|
|
286
|
+
return Math.floor(parsed);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* How long the prewarm queue waits after the gateway is ready before kicking
|
|
291
|
+
* off background spawns. Lets the default app's prebundle get first dibs on
|
|
292
|
+
* CPU. Override via WORKSPACE_PREWARM_DELAY_MS (mostly for tests).
|
|
293
|
+
*/
|
|
294
|
+
export function workspacePrewarmDelayMs(
|
|
295
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
296
|
+
): number {
|
|
297
|
+
const raw = env.WORKSPACE_PREWARM_DELAY_MS;
|
|
298
|
+
if (raw === undefined) return 1_000;
|
|
299
|
+
const parsed = Number(raw);
|
|
300
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 1_000;
|
|
301
|
+
return Math.floor(parsed);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readBooleanEnv(value: string | undefined): boolean | undefined {
|
|
305
|
+
if (value === undefined) return undefined;
|
|
306
|
+
const normalized = value.trim().toLowerCase();
|
|
307
|
+
if (["1", "true", "yes", "on"].includes(normalized)) return true;
|
|
308
|
+
if (["0", "false", "no", "off"].includes(normalized)) return false;
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export type PollingFileWatcherMode =
|
|
313
|
+
| "enable"
|
|
314
|
+
| "disable-explicit"
|
|
315
|
+
| "disable-default";
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Three-way classification of the polling-watcher decision so callers can
|
|
319
|
+
* tell apart "the user explicitly turned this off" (where we want to override
|
|
320
|
+
* any inherited chokidar/TSC env vars from the parent shell) from "we just
|
|
321
|
+
* didn't auto-detect a Builder/Codespaces/Gitpod container" (where the user's
|
|
322
|
+
* own watcher vars should pass through untouched).
|
|
323
|
+
*/
|
|
324
|
+
export function pollingFileWatcherMode(
|
|
325
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
326
|
+
root = process.cwd(),
|
|
327
|
+
): PollingFileWatcherMode {
|
|
328
|
+
const explicit =
|
|
329
|
+
readBooleanEnv(env.AGENT_NATIVE_DEV_USE_POLLING) ??
|
|
330
|
+
readBooleanEnv(env.WORKSPACE_USE_POLLING_WATCHER);
|
|
331
|
+
if (explicit === true) return "enable";
|
|
332
|
+
if (explicit === false) return "disable-explicit";
|
|
333
|
+
|
|
334
|
+
const chokidarExplicit = readBooleanEnv(env.CHOKIDAR_USEPOLLING);
|
|
335
|
+
if (chokidarExplicit === true) return "enable";
|
|
336
|
+
if (chokidarExplicit === false) return "disable-explicit";
|
|
337
|
+
|
|
338
|
+
const autoEnable = Boolean(
|
|
339
|
+
env.BUILDER_IO_DEV_SERVER ||
|
|
340
|
+
env.BUILDER_PROJECT_ID ||
|
|
341
|
+
env.BUILDER_WORKSPACE_ID ||
|
|
342
|
+
env.CODESPACES ||
|
|
343
|
+
env.GITPOD_WORKSPACE_ID ||
|
|
344
|
+
env.REMOTE_CONTAINERS ||
|
|
345
|
+
env.DEVCONTAINER ||
|
|
346
|
+
root.startsWith("/root/app/"),
|
|
347
|
+
);
|
|
348
|
+
return autoEnable ? "enable" : "disable-default";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function shouldUsePollingFileWatcher(
|
|
352
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
353
|
+
root = process.cwd(),
|
|
354
|
+
): boolean {
|
|
355
|
+
return pollingFileWatcherMode(env, root) === "enable";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Default child spawner. On Windows, Node >= 20.12 refuses to spawn the
|
|
360
|
+
* `pnpm` .cmd/.ps1 shims without a shell — the spawn fails ENOENT silently
|
|
361
|
+
* (no `error` listener existed), the app port never binds, and only the
|
|
362
|
+
* readiness timeout surfaced. When the gateway itself was launched by pnpm,
|
|
363
|
+
* `npm_execpath` points at pnpm's JS entry, so run that through the current
|
|
364
|
+
* Node executable instead. Everything else spawns as-is.
|
|
365
|
+
*/
|
|
366
|
+
const windowsSafePnpmSpawn: typeof spawn = ((
|
|
367
|
+
command: string,
|
|
368
|
+
args: readonly string[],
|
|
369
|
+
spawnOptions: object,
|
|
370
|
+
) => {
|
|
371
|
+
if (
|
|
372
|
+
process.platform === "win32" &&
|
|
373
|
+
command === "pnpm" &&
|
|
374
|
+
Array.isArray(args)
|
|
375
|
+
) {
|
|
376
|
+
const execPath = process.env.npm_execpath;
|
|
377
|
+
if (execPath && /\.[cm]?js$/i.test(execPath)) {
|
|
378
|
+
return spawn(process.execPath, [execPath, ...args], spawnOptions);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return spawn(command, args as string[], spawnOptions);
|
|
382
|
+
}) as typeof spawn;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Kill an app child and its WHOLE process tree. `child.kill("SIGTERM")`
|
|
386
|
+
* reaches only the direct pnpm child on Windows; the vite grandchild keeps
|
|
387
|
+
* the app port bound, so the gateway respawned forever into
|
|
388
|
+
* "Port 810x is already in use" (blank apps, dropped sessions). taskkill /T
|
|
389
|
+
* fells the tree; POSIX keeps the plain signal.
|
|
390
|
+
*/
|
|
391
|
+
function killAppProcessTree(child: ChildProcess | undefined): void {
|
|
392
|
+
if (!child) return;
|
|
393
|
+
if (process.platform === "win32" && child.pid) {
|
|
394
|
+
try {
|
|
395
|
+
execFileSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
|
396
|
+
stdio: "ignore",
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
} catch {
|
|
400
|
+
// taskkill unavailable or already exited — fall through to the signal.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
child.kill("SIGTERM");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function devWatcherEnv(
|
|
407
|
+
env: NodeJS.ProcessEnv,
|
|
408
|
+
mode: PollingFileWatcherMode,
|
|
409
|
+
): NodeJS.ProcessEnv {
|
|
410
|
+
if (mode === "enable") {
|
|
411
|
+
return {
|
|
412
|
+
...env,
|
|
413
|
+
CHOKIDAR_USEPOLLING: "1",
|
|
414
|
+
CHOKIDAR_INTERVAL: env.CHOKIDAR_INTERVAL ?? POLLING_WATCH_INTERVAL_MS,
|
|
415
|
+
TSC_WATCHFILE: env.TSC_WATCHFILE ?? "DynamicPriorityPolling",
|
|
416
|
+
TSC_WATCHDIRECTORY: env.TSC_WATCHDIRECTORY ?? "DynamicPriorityPolling",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (mode === "disable-explicit") {
|
|
420
|
+
// The user explicitly turned polling off (AGENT_NATIVE_DEV_USE_POLLING=0
|
|
421
|
+
// / WORKSPACE_USE_POLLING_WATCHER=0 / CHOKIDAR_USEPOLLING=0). Strip the
|
|
422
|
+
// watcher vars from the child env so an inherited parent-shell
|
|
423
|
+
// CHOKIDAR_USEPOLLING=1 (or stale TSC_WATCH* override) can't silently
|
|
424
|
+
// re-enable polling against the user's explicit wish.
|
|
425
|
+
const {
|
|
426
|
+
CHOKIDAR_USEPOLLING: _polling,
|
|
427
|
+
CHOKIDAR_INTERVAL: _interval,
|
|
428
|
+
TSC_WATCHFILE: _watchFile,
|
|
429
|
+
TSC_WATCHDIRECTORY: _watchDir,
|
|
430
|
+
...rest
|
|
431
|
+
} = env;
|
|
432
|
+
return rest;
|
|
433
|
+
}
|
|
434
|
+
// mode === "disable-default": no explicit signal either way. Pass the env
|
|
435
|
+
// through unchanged so legitimate user overrides like
|
|
436
|
+
// TSC_WATCHFILE=UseFsEventsWithFallbackDynamicPolling survive.
|
|
437
|
+
return env;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function initialWorkspaceAppIds(
|
|
441
|
+
apps: Array<Pick<WorkspaceApp, "id">>,
|
|
442
|
+
defaultApp: string,
|
|
443
|
+
eager: boolean,
|
|
444
|
+
startDefault = true,
|
|
445
|
+
): string[] {
|
|
446
|
+
if (eager) return apps.map((app) => app.id);
|
|
447
|
+
if (!startDefault) return [];
|
|
448
|
+
return apps.some((app) => app.id === defaultApp) ? [defaultApp] : [];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function readJson(file: string): any {
|
|
452
|
+
try {
|
|
453
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
454
|
+
} catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function shouldReportDiscoverAppsReadFailure(
|
|
460
|
+
appsDir: string,
|
|
461
|
+
code: string | undefined,
|
|
462
|
+
): boolean {
|
|
463
|
+
if (code === "ENOENT") return false;
|
|
464
|
+
const key = `${appsDir}:${code ?? "unknown"}`;
|
|
465
|
+
if (reportedDiscoverAppsReadFailures.has(key)) return false;
|
|
466
|
+
reportedDiscoverAppsReadFailures.add(key);
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function shouldCaptureDiscoverAppsReadFailure(
|
|
471
|
+
code: string | undefined,
|
|
472
|
+
): boolean {
|
|
473
|
+
return code !== "EACCES" && code !== "EPERM";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function discoverApps(appsDir: string, appPortStart: number): WorkspaceApp[] {
|
|
477
|
+
if (!fs.existsSync(appsDir)) return [];
|
|
478
|
+
// existsSync -> readdirSync is a TOCTOU race. Treat ENOENT as "no apps
|
|
479
|
+
// right now" and let the polling sync recover.
|
|
480
|
+
let entries: fs.Dirent[];
|
|
481
|
+
try {
|
|
482
|
+
entries = fs.readdirSync(appsDir, { withFileTypes: true });
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
485
|
+
if (shouldReportDiscoverAppsReadFailure(appsDir, code)) {
|
|
486
|
+
console.warn(
|
|
487
|
+
`[workspace] Could not read ${appsDir} (${code ?? "unknown"}): ` +
|
|
488
|
+
`${(err as Error).message}`,
|
|
489
|
+
);
|
|
490
|
+
if (shouldCaptureDiscoverAppsReadFailure(code)) {
|
|
491
|
+
Sentry.captureException(err, {
|
|
492
|
+
tags: { handled: "dev-discover-readdir" },
|
|
493
|
+
level: "warning",
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return [];
|
|
498
|
+
}
|
|
499
|
+
return entries
|
|
500
|
+
.filter((entry) => entry.isDirectory())
|
|
501
|
+
.map((entry) => {
|
|
502
|
+
const dir = path.join(appsDir, entry.name);
|
|
503
|
+
const pkg = readJson(path.join(dir, "package.json"));
|
|
504
|
+
if (!pkg) return null;
|
|
505
|
+
const routeAccess = workspaceAppRouteAccessFromPackageJson(pkg);
|
|
506
|
+
return {
|
|
507
|
+
id: entry.name,
|
|
508
|
+
name: pkg.displayName || pkg.name || entry.name,
|
|
509
|
+
description: typeof pkg.description === "string" ? pkg.description : "",
|
|
510
|
+
audience:
|
|
511
|
+
workspaceAppAudienceFromPackageJson(pkg) ??
|
|
512
|
+
DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
513
|
+
publicPaths: routeAccess.publicPaths ?? [],
|
|
514
|
+
protectedPaths: routeAccess.protectedPaths ?? [],
|
|
515
|
+
dir,
|
|
516
|
+
port: appPortStart,
|
|
517
|
+
} satisfies WorkspaceApp;
|
|
518
|
+
})
|
|
519
|
+
.filter((app): app is WorkspaceApp => !!app)
|
|
520
|
+
.sort(compareApps)
|
|
521
|
+
.map((app, index) => ({ ...app, port: appPortStart + index }));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function compareApps(a: Pick<WorkspaceApp, "id">, b: Pick<WorkspaceApp, "id">) {
|
|
525
|
+
if (a.id === "dispatch") return -1;
|
|
526
|
+
if (b.id === "dispatch") return 1;
|
|
527
|
+
return a.id.localeCompare(b.id);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function isChildDevServerUrlLine(line: string): boolean {
|
|
531
|
+
return /^\s*->\s+(?:Local|Network):\s+https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+(?:\/\S*)?\s*$/i.test(
|
|
532
|
+
line.replace(/\u279c/g, "->"),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function formatAppOutput(chunk: unknown): string {
|
|
537
|
+
const lines = String(chunk)
|
|
538
|
+
.split(/\r?\n/)
|
|
539
|
+
.filter(Boolean)
|
|
540
|
+
.filter((line) => !isChildDevServerUrlLine(line));
|
|
541
|
+
return lines.length === 0 ? "" : lines.join("\n") + "\n";
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function appendAppOutputTail(app: WorkspaceApp, output: string): void {
|
|
545
|
+
if (!output) return;
|
|
546
|
+
const next = `${app.outputTail ?? ""}${output}`;
|
|
547
|
+
app.outputTail =
|
|
548
|
+
next.length > APP_OUTPUT_TAIL_BYTES
|
|
549
|
+
? next.slice(-APP_OUTPUT_TAIL_BYTES)
|
|
550
|
+
: next;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function pipeAppOutput(
|
|
554
|
+
prefix: string,
|
|
555
|
+
chunk: unknown,
|
|
556
|
+
write: (value: string) => void,
|
|
557
|
+
): string {
|
|
558
|
+
const output = formatAppOutput(chunk);
|
|
559
|
+
if (!output) return "";
|
|
560
|
+
const prefixed = output
|
|
561
|
+
.trimEnd()
|
|
562
|
+
.split(/\n/)
|
|
563
|
+
.map((line) => `${prefix} ${line}`)
|
|
564
|
+
.join("\n");
|
|
565
|
+
write(`${prefixed}\n`);
|
|
566
|
+
return output;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function firstPathSegment(url: string | undefined): string | null {
|
|
570
|
+
if (!url) return null;
|
|
571
|
+
try {
|
|
572
|
+
const parsed = new URL(url, "http://workspace.local");
|
|
573
|
+
const [segment] = parsed.pathname.split("/").filter(Boolean);
|
|
574
|
+
return segment || null;
|
|
575
|
+
} catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function appRestartDelay(attempts: number): number {
|
|
581
|
+
return Math.min(
|
|
582
|
+
1_000 * 2 ** Math.max(0, attempts - 1),
|
|
583
|
+
APP_RESTART_MAX_DELAY_MS,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function formatProxyReadyTimeout(timeoutMs: number): string {
|
|
588
|
+
const seconds = timeoutMs / 1_000;
|
|
589
|
+
return Number.isInteger(seconds) ? `${seconds}s` : `${timeoutMs}ms`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function probePort(port: number, timeoutMs = 1_000): Promise<boolean> {
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
const socket = new net.Socket();
|
|
595
|
+
let settled = false;
|
|
596
|
+
const finish = (ok: boolean) => {
|
|
597
|
+
if (settled) return;
|
|
598
|
+
settled = true;
|
|
599
|
+
socket.destroy();
|
|
600
|
+
resolve(ok);
|
|
601
|
+
};
|
|
602
|
+
socket.setTimeout(timeoutMs);
|
|
603
|
+
socket.once("connect", () => finish(true));
|
|
604
|
+
socket.once("error", () => finish(false));
|
|
605
|
+
socket.once("timeout", () => finish(false));
|
|
606
|
+
socket.connect(port, "127.0.0.1");
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function probeHttpReady(
|
|
611
|
+
app: Pick<WorkspaceApp, "id" | "port">,
|
|
612
|
+
timeoutMs = 1_000,
|
|
613
|
+
): Promise<boolean> {
|
|
614
|
+
return new Promise((resolve) => {
|
|
615
|
+
let settled = false;
|
|
616
|
+
let req: http.ClientRequest;
|
|
617
|
+
const finish = (ok: boolean) => {
|
|
618
|
+
if (settled) return;
|
|
619
|
+
settled = true;
|
|
620
|
+
req.destroy();
|
|
621
|
+
resolve(ok);
|
|
622
|
+
};
|
|
623
|
+
req = http.request(
|
|
624
|
+
{
|
|
625
|
+
hostname: "127.0.0.1",
|
|
626
|
+
port: app.port,
|
|
627
|
+
method: "GET",
|
|
628
|
+
path: `/${app.id}`,
|
|
629
|
+
headers: {
|
|
630
|
+
accept: "text/html",
|
|
631
|
+
host: `127.0.0.1:${app.port}`,
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
(res) => {
|
|
635
|
+
res.resume();
|
|
636
|
+
finish(true);
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
req.setTimeout(timeoutMs, () => finish(false));
|
|
640
|
+
req.once("error", () => finish(false));
|
|
641
|
+
req.end();
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function firstHeaderValue(
|
|
646
|
+
value: string | string[] | number | undefined,
|
|
647
|
+
): string | undefined {
|
|
648
|
+
if (Array.isArray(value)) return value[0];
|
|
649
|
+
if (value === undefined) return undefined;
|
|
650
|
+
return String(value);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function wantsHtml(req: http.IncomingMessage): boolean {
|
|
654
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
655
|
+
const accept = firstHeaderValue(req.headers.accept);
|
|
656
|
+
if (!accept) return false;
|
|
657
|
+
return accept.includes("text/html");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function renderStartingApp(app: WorkspaceApp): string {
|
|
661
|
+
const escapedName = escapeHtml(app.name || app.id);
|
|
662
|
+
const failure = app.lastFailure;
|
|
663
|
+
const retryDelayMs = failure
|
|
664
|
+
? Math.max(1_000, failure.nextRetryAt - Date.now() + 250)
|
|
665
|
+
: 900;
|
|
666
|
+
const refreshSeconds = failure
|
|
667
|
+
? Math.max(1, Math.ceil(retryDelayMs / 1_000))
|
|
668
|
+
: 1;
|
|
669
|
+
const refreshScriptDelay = failure ? retryDelayMs : 900;
|
|
670
|
+
const title = failure
|
|
671
|
+
? `${failure.installing ? "Install failed" : "App failed to start"}: ${escapedName}`
|
|
672
|
+
: `Starting ${escapedName}`;
|
|
673
|
+
const message = failure
|
|
674
|
+
? `The workspace gateway will retry in ${Math.max(
|
|
675
|
+
1,
|
|
676
|
+
Math.ceil((failure.nextRetryAt - Date.now()) / 1_000),
|
|
677
|
+
)}s. Fix the error below or stop the server with Ctrl+C.`
|
|
678
|
+
: app.installing
|
|
679
|
+
? "The workspace gateway is installing this app's dependencies before starting it."
|
|
680
|
+
: app.building
|
|
681
|
+
? "The workspace gateway is building this app's production server before serving it."
|
|
682
|
+
: "The workspace gateway is waking this app's dev server.";
|
|
683
|
+
const failureOutput = failure?.output.trim();
|
|
684
|
+
return `<!doctype html>
|
|
685
|
+
<html>
|
|
686
|
+
<head>
|
|
687
|
+
<meta charset="utf-8" />
|
|
688
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
689
|
+
<meta http-equiv="refresh" content="${refreshSeconds}" />
|
|
690
|
+
<title>${title}</title>
|
|
691
|
+
<meta name="color-scheme" content="light dark" />
|
|
692
|
+
<style>
|
|
693
|
+
:root { --bg: #fafafa; --fg: #171717; --muted: #737373; --bar-bg: #e5e5e5; --bar-fill: #171717; --danger: #dc2626; --code-bg: #171717; --code-fg: #fafafa; }
|
|
694
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0a0a0a; --fg: #fafafa; --muted: #a3a3a3; --bar-bg: #262626; --bar-fill: #fafafa; --danger: #f87171; --code-bg: #171717; --code-fg: #f5f5f5; } }
|
|
695
|
+
body { min-height: 100vh; margin: 0; display: grid; place-items: center; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--fg); }
|
|
696
|
+
main { width: min(680px, calc(100vw - 48px)); }
|
|
697
|
+
.bar { height: 3px; overflow: hidden; border-radius: 999px; background: var(--bar-bg); }
|
|
698
|
+
.bar::before { content: ""; display: block; height: 100%; width: 42%; border-radius: inherit; background: var(--bar-fill); animation: load 1s ease-in-out infinite; }
|
|
699
|
+
main.failed .bar::before { width: 100%; background: var(--danger); animation: none; }
|
|
700
|
+
p { color: var(--muted); }
|
|
701
|
+
pre { max-height: min(46vh, 360px); overflow: auto; margin-top: 20px; padding: 14px 16px; border-radius: 8px; background: var(--code-bg); color: var(--code-fg); font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; white-space: pre-wrap; word-break: break-word; }
|
|
702
|
+
@keyframes load { 0% { transform: translateX(-105%); } 100% { transform: translateX(245%); } }
|
|
703
|
+
</style>
|
|
704
|
+
<script>
|
|
705
|
+
(() => {
|
|
706
|
+
const reload = () => window.location.reload();
|
|
707
|
+
setTimeout(reload, ${JSON.stringify(refreshScriptDelay)});
|
|
708
|
+
setInterval(reload, ${JSON.stringify(Math.max(refreshScriptDelay, 3_000))});
|
|
709
|
+
})();
|
|
710
|
+
</script>
|
|
711
|
+
</head>
|
|
712
|
+
<body>
|
|
713
|
+
<main class="${failure ? "failed" : ""}">
|
|
714
|
+
<div class="bar"></div>
|
|
715
|
+
<h1>${title}</h1>
|
|
716
|
+
<p>${escapeHtml(message)}</p>
|
|
717
|
+
${failureOutput ? `<pre>${escapeHtml(failureOutput)}</pre>` : ""}
|
|
718
|
+
</main>
|
|
719
|
+
</body>
|
|
720
|
+
</html>`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function renderIndex(apps: WorkspaceApp[]): string {
|
|
724
|
+
return `<!doctype html>
|
|
725
|
+
<html>
|
|
726
|
+
<head>
|
|
727
|
+
<meta charset="utf-8" />
|
|
728
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
729
|
+
<title>Agent-Native Workspace</title>
|
|
730
|
+
<meta name="color-scheme" content="light dark" />
|
|
731
|
+
<style>
|
|
732
|
+
:root { --bg: #fafafa; --fg: #171717; --muted: #737373; --card-bg: #ffffff; --card-border: #d4d4d4; }
|
|
733
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0a0a0a; --fg: #fafafa; --muted: #a3a3a3; --card-bg: #171717; --card-border: #262626; } }
|
|
734
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 32px; background: var(--bg); color: var(--fg); }
|
|
735
|
+
main { max-width: 760px; margin: 0 auto; }
|
|
736
|
+
a { color: inherit; text-decoration: none; }
|
|
737
|
+
.grid { display: grid; gap: 12px; margin-top: 20px; }
|
|
738
|
+
.card { display: flex; justify-content: space-between; border: 1px solid var(--card-border); border-radius: 8px; padding: 14px 16px; background: var(--card-bg); }
|
|
739
|
+
.muted { color: var(--muted); }
|
|
740
|
+
</style>
|
|
741
|
+
</head>
|
|
742
|
+
<body>
|
|
743
|
+
<main>
|
|
744
|
+
<h1>Agent-Native Workspace</h1>
|
|
745
|
+
<p class="muted">Open an app below. Dispatch is the workspace control plane when installed.</p>
|
|
746
|
+
<div class="grid">
|
|
747
|
+
${apps
|
|
748
|
+
.map(
|
|
749
|
+
(app) =>
|
|
750
|
+
`<a class="card" href="/${app.id}"><strong>${escapeHtml(app.name)}</strong><span class="muted">/${escapeHtml(app.id)}</span></a>`,
|
|
751
|
+
)
|
|
752
|
+
.join("")}
|
|
753
|
+
</div>
|
|
754
|
+
</main>
|
|
755
|
+
</body>
|
|
756
|
+
</html>`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function hasLocalBin(dir: string, command: string): boolean {
|
|
760
|
+
const binDir = path.join(dir, "node_modules", ".bin");
|
|
761
|
+
return (
|
|
762
|
+
fs.existsSync(path.join(binDir, command)) ||
|
|
763
|
+
fs.existsSync(path.join(binDir, `${command}.cmd`)) ||
|
|
764
|
+
fs.existsSync(path.join(binDir, `${command}.ps1`))
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export async function runWorkspaceDev(
|
|
769
|
+
options: WorkspaceDevOptions = {},
|
|
770
|
+
): Promise<WorkspaceDevHandle> {
|
|
771
|
+
const args = options.args ?? process.argv.slice(2);
|
|
772
|
+
const env = options.env ?? process.env;
|
|
773
|
+
const root = options.root ?? process.cwd();
|
|
774
|
+
const appsDir = path.join(root, "apps");
|
|
775
|
+
const spawnProcess = options.spawnProcess ?? windowsSafePnpmSpawn;
|
|
776
|
+
const stdout = options.stdout ?? process.stdout;
|
|
777
|
+
const stderr = options.stderr ?? process.stderr;
|
|
778
|
+
|
|
779
|
+
fs.mkdirSync(path.join(root, "data"), { recursive: true });
|
|
780
|
+
const gatewayHost = env.WORKSPACE_HOST || DEFAULT_GATEWAY_HOST;
|
|
781
|
+
const requestedPort = Number(
|
|
782
|
+
env.WORKSPACE_PORT || env.PORT || DEFAULT_GATEWAY_PORT,
|
|
783
|
+
);
|
|
784
|
+
const appPortStart = Number(
|
|
785
|
+
env.WORKSPACE_APP_PORT_START || DEFAULT_APP_PORT_START,
|
|
786
|
+
);
|
|
787
|
+
const forceVite = env.WORKSPACE_VITE_FORCE === "1";
|
|
788
|
+
const eager = shouldEagerStartWorkspaceApps(args, env);
|
|
789
|
+
const builtMode = shouldRunBuiltWorkspaceApps(args, env);
|
|
790
|
+
const pollingMode = pollingFileWatcherMode(env, root);
|
|
791
|
+
const usePollingFileWatcher = pollingMode === "enable";
|
|
792
|
+
const proxyReadyTimeoutMs = Number(
|
|
793
|
+
env.WORKSPACE_PROXY_READY_TIMEOUT_MS ?? 30_000,
|
|
794
|
+
);
|
|
795
|
+
const proxyResponseTimeoutMs = Number(
|
|
796
|
+
env.WORKSPACE_PROXY_RESPONSE_TIMEOUT_MS ??
|
|
797
|
+
DEFAULT_PROXY_RESPONSE_TIMEOUT_MS,
|
|
798
|
+
);
|
|
799
|
+
const proxyNonHtmlResponseTimeoutMs = Number(
|
|
800
|
+
env.WORKSPACE_PROXY_NON_HTML_RESPONSE_TIMEOUT_MS ??
|
|
801
|
+
env.WORKSPACE_PROXY_RESPONSE_TIMEOUT_MS ??
|
|
802
|
+
DEFAULT_PROXY_NON_HTML_RESPONSE_TIMEOUT_MS,
|
|
803
|
+
);
|
|
804
|
+
let gatewayUrl = `http://${gatewayHost}:${requestedPort}`;
|
|
805
|
+
|
|
806
|
+
const apps = discoverApps(appsDir, appPortStart);
|
|
807
|
+
if (apps.length === 0) {
|
|
808
|
+
throw new Error("[workspace] No apps found under ./apps");
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Probe each app's proposed port to see if something else on the host
|
|
812
|
+
// already owns it. Vite is spawned with `--strictPort` (so the gateway can
|
|
813
|
+
// route /<appId> to a known port), which fails hard on EADDRINUSE — without
|
|
814
|
+
// this probe a single conflicting process on 8100/8101/... kills the
|
|
815
|
+
// workspace before the dev server prints anything useful.
|
|
816
|
+
function probePortAvailable(port: number): Promise<boolean> {
|
|
817
|
+
return new Promise((resolve) => {
|
|
818
|
+
const probe = net.createServer();
|
|
819
|
+
probe.once("error", () => resolve(false));
|
|
820
|
+
probe.once("listening", () => {
|
|
821
|
+
probe.close(() => resolve(true));
|
|
822
|
+
});
|
|
823
|
+
probe.listen(port, gatewayHost);
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function reserveAppPort(
|
|
828
|
+
start: number,
|
|
829
|
+
excluded: Set<number>,
|
|
830
|
+
): Promise<number> {
|
|
831
|
+
for (let port = start; port < start + 100; port++) {
|
|
832
|
+
if (excluded.has(port)) continue;
|
|
833
|
+
if (port === requestedPort) continue;
|
|
834
|
+
if (await probePortAvailable(port)) return port;
|
|
835
|
+
}
|
|
836
|
+
throw new Error(
|
|
837
|
+
`[workspace] No available port found between ${start} and ${start + 100}`,
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const reservedAppPorts = new Set<number>();
|
|
842
|
+
for (const app of apps) {
|
|
843
|
+
const original = app.port;
|
|
844
|
+
const port = await reserveAppPort(original, reservedAppPorts);
|
|
845
|
+
if (port !== original) {
|
|
846
|
+
stdout.write(
|
|
847
|
+
`[workspace] Port ${original} unavailable for /${app.id}, using ${port}\n`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
app.port = port;
|
|
851
|
+
reservedAppPorts.add(port);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const appById = new Map(apps.map((app) => [app.id, app]));
|
|
855
|
+
const explicitDefaultApp =
|
|
856
|
+
env.WORKSPACE_DEFAULT_APP && appById.has(env.WORKSPACE_DEFAULT_APP)
|
|
857
|
+
? env.WORKSPACE_DEFAULT_APP
|
|
858
|
+
: null;
|
|
859
|
+
const hasDispatch = appById.has("dispatch");
|
|
860
|
+
const defaultApp =
|
|
861
|
+
explicitDefaultApp ?? (hasDispatch ? "dispatch" : apps[0].id);
|
|
862
|
+
const redirectRootToDefault = Boolean(explicitDefaultApp || hasDispatch);
|
|
863
|
+
|
|
864
|
+
let syncTimer: NodeJS.Timeout | undefined;
|
|
865
|
+
let shuttingDown = false;
|
|
866
|
+
let workspaceStarted = false;
|
|
867
|
+
|
|
868
|
+
if (usePollingFileWatcher) {
|
|
869
|
+
stdout.write(
|
|
870
|
+
`[workspace] Using polling file watchers (${POLLING_WATCH_INTERVAL_MS}ms) to avoid remote-container inotify limits.\n`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
let readyResolve: (value: { port: number; url: string }) => void;
|
|
875
|
+
const ready = new Promise<{ port: number; url: string }>((resolve) => {
|
|
876
|
+
readyResolve = resolve;
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
function workspaceAppsJson(): string {
|
|
880
|
+
return JSON.stringify(
|
|
881
|
+
apps.map((workspaceApp) => ({
|
|
882
|
+
id: workspaceApp.id,
|
|
883
|
+
name: workspaceApp.name,
|
|
884
|
+
description: workspaceApp.description,
|
|
885
|
+
path: `/${workspaceApp.id}`,
|
|
886
|
+
audience: workspaceApp.audience,
|
|
887
|
+
publicPaths: workspaceApp.publicPaths,
|
|
888
|
+
protectedPaths: workspaceApp.protectedPaths,
|
|
889
|
+
})),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function syncApps(): Promise<void> {
|
|
894
|
+
const discovered = discoverApps(appsDir, appPortStart);
|
|
895
|
+
for (const app of discovered) {
|
|
896
|
+
const existing = appById.get(app.id);
|
|
897
|
+
if (existing) {
|
|
898
|
+
existing.name = app.name;
|
|
899
|
+
existing.description = app.description;
|
|
900
|
+
existing.audience = app.audience;
|
|
901
|
+
existing.publicPaths = app.publicPaths;
|
|
902
|
+
existing.protectedPaths = app.protectedPaths;
|
|
903
|
+
existing.dir = app.dir;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const usedPorts = new Set(apps.map((existingApp) => existingApp.port));
|
|
907
|
+
const port = await reserveAppPort(appPortStart, usedPorts);
|
|
908
|
+
reservedAppPorts.add(port);
|
|
909
|
+
const next = { ...app, port };
|
|
910
|
+
apps.push(next);
|
|
911
|
+
apps.sort(compareApps);
|
|
912
|
+
appById.set(next.id, next);
|
|
913
|
+
stdout.write(`[workspace] Detected new app: /${next.id}\n`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function scheduleSync(): void {
|
|
918
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
919
|
+
syncTimer = setTimeout(() => {
|
|
920
|
+
void syncApps().catch((err) => {
|
|
921
|
+
stderr.write(
|
|
922
|
+
`[workspace] App sync failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
}, 400);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function appForRequest(req: http.IncomingMessage): WorkspaceApp | null {
|
|
929
|
+
const params = new URL(req.url || "/", "http://workspace.local")
|
|
930
|
+
.searchParams;
|
|
931
|
+
const explicit = params.get("_app");
|
|
932
|
+
if (explicit && appById.has(explicit)) return appById.get(explicit) ?? null;
|
|
933
|
+
|
|
934
|
+
const direct = firstPathSegment(req.url);
|
|
935
|
+
if (direct && appById.has(direct)) return appById.get(direct) ?? null;
|
|
936
|
+
|
|
937
|
+
const fromState = extractOAuthStateAppId(params.get("state"));
|
|
938
|
+
if (fromState && appById.has(fromState)) {
|
|
939
|
+
return appById.get(fromState) ?? null;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const referer = req.headers.referer;
|
|
943
|
+
const fromReferer =
|
|
944
|
+
typeof referer === "string" ? firstPathSegment(referer) : null;
|
|
945
|
+
return fromReferer && appById.has(fromReferer)
|
|
946
|
+
? (appById.get(fromReferer) ?? null)
|
|
947
|
+
: null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function startApp(app: WorkspaceApp): void {
|
|
951
|
+
if (app.process && !app.process.killed) return;
|
|
952
|
+
if (app.restartTimer) return;
|
|
953
|
+
app.lastFailure = undefined;
|
|
954
|
+
app.outputTail = undefined;
|
|
955
|
+
|
|
956
|
+
const basePath = `/${app.id}`;
|
|
957
|
+
const shouldInstall =
|
|
958
|
+
!app.installAttempted && !hasLocalBin(app.dir, "vite");
|
|
959
|
+
const builtEntry = builtWorkspaceAppServerEntry(app.dir);
|
|
960
|
+
let shouldBuild = false;
|
|
961
|
+
let runBuilt = false;
|
|
962
|
+
if (builtMode && !shouldInstall && !app.devMode) {
|
|
963
|
+
let hasFreshBuild = false;
|
|
964
|
+
if (fs.existsSync(builtEntry)) {
|
|
965
|
+
if (app.buildChecked) {
|
|
966
|
+
hasFreshBuild = true;
|
|
967
|
+
} else {
|
|
968
|
+
app.buildChecked = true;
|
|
969
|
+
try {
|
|
970
|
+
hasFreshBuild =
|
|
971
|
+
newestBuiltModeSourceMtimeMs(app.dir) <=
|
|
972
|
+
fs.statSync(builtEntry).mtimeMs;
|
|
973
|
+
} catch {
|
|
974
|
+
hasFreshBuild = false;
|
|
975
|
+
}
|
|
976
|
+
if (!hasFreshBuild) {
|
|
977
|
+
stdout.write(
|
|
978
|
+
`[workspace] /${app.id}: production build is stale \u2014 rebuilding before serving\n`,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (hasFreshBuild) {
|
|
984
|
+
runBuilt = true;
|
|
985
|
+
} else if (!app.buildAttempted) {
|
|
986
|
+
shouldBuild = true;
|
|
987
|
+
} else {
|
|
988
|
+
app.devMode = true;
|
|
989
|
+
stdout.write(
|
|
990
|
+
`[workspace] /${app.id}: no usable production build \u2014 falling back to the vite dev server\n`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
let spawnCommand = "pnpm";
|
|
996
|
+
let childArgs: string[];
|
|
997
|
+
if (shouldInstall) {
|
|
998
|
+
childArgs = [
|
|
999
|
+
"--dir",
|
|
1000
|
+
root,
|
|
1001
|
+
"install",
|
|
1002
|
+
"--no-frozen-lockfile",
|
|
1003
|
+
"--prefer-offline",
|
|
1004
|
+
];
|
|
1005
|
+
} else if (shouldBuild) {
|
|
1006
|
+
childArgs = ["--dir", app.dir, "run", "build"];
|
|
1007
|
+
} else if (runBuilt) {
|
|
1008
|
+
spawnCommand = process.execPath;
|
|
1009
|
+
childArgs = [builtEntry];
|
|
1010
|
+
} else {
|
|
1011
|
+
childArgs = [
|
|
1012
|
+
"--dir",
|
|
1013
|
+
app.dir,
|
|
1014
|
+
"exec",
|
|
1015
|
+
"vite",
|
|
1016
|
+
"--host",
|
|
1017
|
+
"127.0.0.1",
|
|
1018
|
+
"--port",
|
|
1019
|
+
String(app.port),
|
|
1020
|
+
"--strictPort",
|
|
1021
|
+
...(forceVite ? ["--force"] : []),
|
|
1022
|
+
];
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (shouldInstall) {
|
|
1026
|
+
stdout.write(
|
|
1027
|
+
`[workspace] Installing dependencies before starting /${app.id}\n`,
|
|
1028
|
+
);
|
|
1029
|
+
} else if (shouldBuild) {
|
|
1030
|
+
stdout.write(
|
|
1031
|
+
`[workspace] Building /${app.id} for built-mode serving (one-time; subsequent boots are instant)\n`,
|
|
1032
|
+
);
|
|
1033
|
+
} else if (runBuilt) {
|
|
1034
|
+
stdout.write(`[workspace] /${app.id}: serving prebuilt server\n`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const child = spawnProcess(spawnCommand, childArgs, {
|
|
1038
|
+
cwd: root,
|
|
1039
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1040
|
+
env: devWatcherEnv(
|
|
1041
|
+
{
|
|
1042
|
+
...env,
|
|
1043
|
+
APP_NAME: app.id,
|
|
1044
|
+
AGENT_NATIVE_WORKSPACE: "1",
|
|
1045
|
+
AGENT_NATIVE_WORKSPACE_APP_ID: app.id,
|
|
1046
|
+
AGENT_NATIVE_WORKSPACE_APPS_JSON: workspaceAppsJson(),
|
|
1047
|
+
AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: app.audience,
|
|
1048
|
+
AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(
|
|
1049
|
+
app.publicPaths,
|
|
1050
|
+
),
|
|
1051
|
+
AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(
|
|
1052
|
+
app.protectedPaths,
|
|
1053
|
+
),
|
|
1054
|
+
APP_BASE_PATH: basePath,
|
|
1055
|
+
VITE_AGENT_NATIVE_WORKSPACE: "1",
|
|
1056
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_ID: app.id,
|
|
1057
|
+
VITE_AGENT_NATIVE_WORKSPACE_APPS_JSON: workspaceAppsJson(),
|
|
1058
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_AUDIENCE: app.audience,
|
|
1059
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PUBLIC_PATHS: JSON.stringify(
|
|
1060
|
+
app.publicPaths,
|
|
1061
|
+
),
|
|
1062
|
+
VITE_AGENT_NATIVE_WORKSPACE_APP_PROTECTED_PATHS: JSON.stringify(
|
|
1063
|
+
app.protectedPaths,
|
|
1064
|
+
),
|
|
1065
|
+
VITE_APP_BASE_PATH: basePath,
|
|
1066
|
+
VITE_WORKSPACE_OAUTH_ORIGIN: workspaceOAuthOrigin(env, gatewayUrl),
|
|
1067
|
+
VITE_WORKSPACE_GATEWAY_URL: gatewayUrl,
|
|
1068
|
+
PORT: String(app.port),
|
|
1069
|
+
WORKSPACE_GATEWAY_URL: gatewayUrl,
|
|
1070
|
+
...(runBuilt ? { HOST: "127.0.0.1" } : {}),
|
|
1071
|
+
},
|
|
1072
|
+
pollingMode,
|
|
1073
|
+
),
|
|
1074
|
+
});
|
|
1075
|
+
app.process = child;
|
|
1076
|
+
app.installing = shouldInstall;
|
|
1077
|
+
app.building = shouldBuild;
|
|
1078
|
+
if (runBuilt) attachPromoteWatcher(app);
|
|
1079
|
+
|
|
1080
|
+
const prefix = `[${app.id}]`;
|
|
1081
|
+
const stableTimer = setTimeout(() => {
|
|
1082
|
+
app.restartAttempts = 0;
|
|
1083
|
+
}, 5_000);
|
|
1084
|
+
stableTimer.unref();
|
|
1085
|
+
|
|
1086
|
+
child.stdout?.on("data", (chunk) => {
|
|
1087
|
+
appendAppOutputTail(
|
|
1088
|
+
app,
|
|
1089
|
+
pipeAppOutput(prefix, chunk, (value) => stdout.write(value)),
|
|
1090
|
+
);
|
|
1091
|
+
});
|
|
1092
|
+
child.stderr?.on("data", (chunk) => {
|
|
1093
|
+
appendAppOutputTail(
|
|
1094
|
+
app,
|
|
1095
|
+
pipeAppOutput(prefix, chunk, (value) => stderr.write(value)),
|
|
1096
|
+
);
|
|
1097
|
+
});
|
|
1098
|
+
child.on("error", (error) => {
|
|
1099
|
+
// Without this listener a failed spawn (e.g. the Windows pnpm-shim
|
|
1100
|
+
// ENOENT) was SILENT — no exit event fires, the port never binds, and
|
|
1101
|
+
// only the readiness timeout showed. Surface it and enter the normal
|
|
1102
|
+
// retry path.
|
|
1103
|
+
clearTimeout(stableTimer);
|
|
1104
|
+
const wasInstalling = app.installing;
|
|
1105
|
+
app.process = undefined;
|
|
1106
|
+
app.installing = false;
|
|
1107
|
+
app.building = false;
|
|
1108
|
+
app.ready = false;
|
|
1109
|
+
app.readinessProbe = undefined;
|
|
1110
|
+
if (app.restartTimer || shuttingDown) return;
|
|
1111
|
+
if (wasInstalling) app.installAttempted = false;
|
|
1112
|
+
scheduleAppRestart(app, {
|
|
1113
|
+
code: null,
|
|
1114
|
+
signal: null,
|
|
1115
|
+
installing: wasInstalling,
|
|
1116
|
+
output: String(error),
|
|
1117
|
+
logMessage: `failed to spawn child process: ${
|
|
1118
|
+
error instanceof Error ? error.message : String(error)
|
|
1119
|
+
}`,
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
child.on("exit", (code, signal) => {
|
|
1123
|
+
clearTimeout(stableTimer);
|
|
1124
|
+
const wasInstalling = app.installing;
|
|
1125
|
+
const wasBuilding = app.building;
|
|
1126
|
+
app.process = undefined;
|
|
1127
|
+
app.installing = false;
|
|
1128
|
+
app.building = false;
|
|
1129
|
+
app.ready = false;
|
|
1130
|
+
app.readinessProbe = undefined;
|
|
1131
|
+
if (app.expectedExit) {
|
|
1132
|
+
// Intentional kill (promotion to the vite dev server). Respawn
|
|
1133
|
+
// immediately instead of treating it as a crash.
|
|
1134
|
+
app.expectedExit = false;
|
|
1135
|
+
if (!shuttingDown && !app.restartTimer) startApp(app);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (app.restartTimer) return;
|
|
1139
|
+
if (code === 0 || shuttingDown) {
|
|
1140
|
+
if (code === 0 && !shuttingDown) {
|
|
1141
|
+
if (wasInstalling) {
|
|
1142
|
+
app.installAttempted = true;
|
|
1143
|
+
startApp(app);
|
|
1144
|
+
} else if (wasBuilding) {
|
|
1145
|
+
app.buildAttempted = true;
|
|
1146
|
+
startApp(app);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (wasInstalling) app.installAttempted = false;
|
|
1152
|
+
if (wasBuilding) {
|
|
1153
|
+
// A failed production build must never brick the app — fall back to
|
|
1154
|
+
// the vite dev server, which surfaces the error with hot reload.
|
|
1155
|
+
app.devMode = true;
|
|
1156
|
+
stderr.write(
|
|
1157
|
+
`[${app.id}] production build failed (exit ${code}); falling back to the vite dev server\n`,
|
|
1158
|
+
);
|
|
1159
|
+
startApp(app);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
scheduleAppRestart(app, {
|
|
1163
|
+
code,
|
|
1164
|
+
signal,
|
|
1165
|
+
installing: wasInstalling,
|
|
1166
|
+
output: app.outputTail ?? "",
|
|
1167
|
+
logMessage: `exited with code ${code}`,
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Built-mode promotion: the moment one of an app's source files changes,
|
|
1174
|
+
* swap its prebuilt server for the vite dev server on the same port. The
|
|
1175
|
+
* proxy doesn't care what listens upstream, so the swap is invisible apart
|
|
1176
|
+
* from a brief wake page. Sticky for the rest of the session.
|
|
1177
|
+
*/
|
|
1178
|
+
function promoteAppToDevServer(app: WorkspaceApp, changedPath: string): void {
|
|
1179
|
+
if (app.devMode || shuttingDown) return;
|
|
1180
|
+
app.devMode = true;
|
|
1181
|
+
app.promoteWatcher?.close();
|
|
1182
|
+
app.promoteWatcher = undefined;
|
|
1183
|
+
stdout.write(
|
|
1184
|
+
`[workspace] /${app.id}: source changed (${changedPath}) \u2014 promoting to the vite dev server with hot reload\n`,
|
|
1185
|
+
);
|
|
1186
|
+
app.ready = false;
|
|
1187
|
+
app.readinessProbe = undefined;
|
|
1188
|
+
if (app.process && !app.process.killed) {
|
|
1189
|
+
app.expectedExit = true;
|
|
1190
|
+
killAppProcessTree(app.process);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
startApp(app);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function attachPromoteWatcher(app: WorkspaceApp): void {
|
|
1197
|
+
if (app.promoteWatcher || app.devMode) return;
|
|
1198
|
+
try {
|
|
1199
|
+
const watcher = fs.watch(
|
|
1200
|
+
app.dir,
|
|
1201
|
+
{ recursive: true },
|
|
1202
|
+
(_event, filename) => {
|
|
1203
|
+
const rel = filename ? String(filename) : "";
|
|
1204
|
+
if (!isBuiltModeSourcePath(rel)) return;
|
|
1205
|
+
promoteAppToDevServer(app, rel);
|
|
1206
|
+
},
|
|
1207
|
+
);
|
|
1208
|
+
watcher.on("error", (err) => {
|
|
1209
|
+
handleWatcherError(err as NodeJS.ErrnoException);
|
|
1210
|
+
});
|
|
1211
|
+
app.promoteWatcher = watcher;
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
handleWatcherError(err as NodeJS.ErrnoException);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function scheduleAppRestart(
|
|
1218
|
+
app: WorkspaceApp,
|
|
1219
|
+
input: {
|
|
1220
|
+
code: number | null;
|
|
1221
|
+
signal: NodeJS.Signals | null;
|
|
1222
|
+
installing: boolean;
|
|
1223
|
+
output: string;
|
|
1224
|
+
logMessage: string;
|
|
1225
|
+
},
|
|
1226
|
+
): void {
|
|
1227
|
+
if (shuttingDown || app.restartTimer) return;
|
|
1228
|
+
if (input.installing) app.installAttempted = false;
|
|
1229
|
+
app.restartAttempts = (app.restartAttempts ?? 0) + 1;
|
|
1230
|
+
const delay = appRestartDelay(app.restartAttempts);
|
|
1231
|
+
const nextRetryAt = Date.now() + delay;
|
|
1232
|
+
app.lastFailure = {
|
|
1233
|
+
code: input.code,
|
|
1234
|
+
signal: input.signal,
|
|
1235
|
+
at: Date.now(),
|
|
1236
|
+
installing: input.installing,
|
|
1237
|
+
output: input.output,
|
|
1238
|
+
nextRetryAt,
|
|
1239
|
+
};
|
|
1240
|
+
stderr.write(
|
|
1241
|
+
`[${app.id}] ${input.logMessage}; retrying in ${Math.round(
|
|
1242
|
+
delay / 1000,
|
|
1243
|
+
)}s\n`,
|
|
1244
|
+
);
|
|
1245
|
+
app.restartTimer = setTimeout(() => {
|
|
1246
|
+
app.restartTimer = undefined;
|
|
1247
|
+
startApp(app);
|
|
1248
|
+
}, delay);
|
|
1249
|
+
app.restartTimer.unref();
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function failAppStartupTimeout(app: WorkspaceApp): void {
|
|
1253
|
+
if (app.installing || app.building || app.ready || app.restartTimer) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const timeout = formatProxyReadyTimeout(proxyReadyTimeoutMs);
|
|
1257
|
+
const message =
|
|
1258
|
+
`Timed out waiting ${timeout} for /${app.id} to return ` +
|
|
1259
|
+
`an HTTP response on 127.0.0.1:${app.port}.`;
|
|
1260
|
+
const output = [message, app.outputTail?.trim()]
|
|
1261
|
+
.filter(Boolean)
|
|
1262
|
+
.join("\n\nLast child output:\n");
|
|
1263
|
+
app.ready = false;
|
|
1264
|
+
app.readinessProbe = undefined;
|
|
1265
|
+
scheduleAppRestart(app, {
|
|
1266
|
+
code: null,
|
|
1267
|
+
signal: null,
|
|
1268
|
+
installing: false,
|
|
1269
|
+
output,
|
|
1270
|
+
logMessage: message,
|
|
1271
|
+
});
|
|
1272
|
+
killAppProcessTree(app.process);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function forwardedProto(req: http.IncomingMessage): string {
|
|
1276
|
+
return (
|
|
1277
|
+
firstHeaderValue(req.headers["x-forwarded-proto"]) ||
|
|
1278
|
+
((req.socket as { encrypted?: boolean }).encrypted ? "https" : "http")
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function forwardedHost(req: http.IncomingMessage): string {
|
|
1283
|
+
return (
|
|
1284
|
+
firstHeaderValue(req.headers["x-forwarded-host"]) ||
|
|
1285
|
+
firstHeaderValue(req.headers.host) ||
|
|
1286
|
+
new URL(gatewayUrl).host
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function proxyHeaders(
|
|
1291
|
+
req: http.IncomingMessage,
|
|
1292
|
+
targetHost: string,
|
|
1293
|
+
): http.OutgoingHttpHeaders {
|
|
1294
|
+
return {
|
|
1295
|
+
...req.headers,
|
|
1296
|
+
"x-forwarded-host": forwardedHost(req),
|
|
1297
|
+
"x-forwarded-proto": forwardedProto(req),
|
|
1298
|
+
host: targetHost,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async function waitForPort(port: number, deadline: number): Promise<boolean> {
|
|
1303
|
+
while (Date.now() < deadline) {
|
|
1304
|
+
if (await probePort(port)) return true;
|
|
1305
|
+
await new Promise((r) => setTimeout(r, PROXY_READY_RETRY_DELAY_MS));
|
|
1306
|
+
}
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async function waitForHttpReady(
|
|
1311
|
+
app: WorkspaceApp,
|
|
1312
|
+
deadline: number,
|
|
1313
|
+
): Promise<boolean> {
|
|
1314
|
+
while (Date.now() < deadline) {
|
|
1315
|
+
const timeoutMs = Math.min(1_000, Math.max(1, deadline - Date.now()));
|
|
1316
|
+
if (await probeHttpReady(app, timeoutMs)) return true;
|
|
1317
|
+
await new Promise((r) => setTimeout(r, PROXY_READY_RETRY_DELAY_MS));
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function ensureReadinessProbe(app: WorkspaceApp): void {
|
|
1323
|
+
if (app.ready || app.readinessProbe || app.installing || app.building) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
app.readinessProbe = waitForHttpReady(app, Date.now() + proxyReadyTimeoutMs)
|
|
1327
|
+
.then((ready) => {
|
|
1328
|
+
if (ready) {
|
|
1329
|
+
app.ready = true;
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
failAppStartupTimeout(app);
|
|
1333
|
+
})
|
|
1334
|
+
.finally(() => {
|
|
1335
|
+
app.readinessProbe = undefined;
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function proxyHttp(
|
|
1340
|
+
app: WorkspaceApp,
|
|
1341
|
+
req: http.IncomingMessage,
|
|
1342
|
+
res: http.ServerResponse,
|
|
1343
|
+
): void {
|
|
1344
|
+
const cold = !app.process || app.process.killed;
|
|
1345
|
+
startApp(app);
|
|
1346
|
+
|
|
1347
|
+
if (!app.ready && wantsHtml(req)) {
|
|
1348
|
+
ensureReadinessProbe(app);
|
|
1349
|
+
res.writeHead(200, STARTING_APP_RESPONSE_HEADERS);
|
|
1350
|
+
if (req.method === "HEAD") {
|
|
1351
|
+
res.end();
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
res.end(renderStartingApp(app));
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const serveStartingPage = () => {
|
|
1359
|
+
res.writeHead(200, STARTING_APP_RESPONSE_HEADERS);
|
|
1360
|
+
if (req.method === "HEAD") {
|
|
1361
|
+
res.end();
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
res.end(renderStartingApp(app));
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const dispatch = () => {
|
|
1368
|
+
const headers = proxyHeaders(req, `127.0.0.1:${app.port}`);
|
|
1369
|
+
let settled = false;
|
|
1370
|
+
let responseTimer: NodeJS.Timeout;
|
|
1371
|
+
const responseTimeoutMs = wantsHtml(req)
|
|
1372
|
+
? proxyResponseTimeoutMs
|
|
1373
|
+
: proxyNonHtmlResponseTimeoutMs;
|
|
1374
|
+
const proxyReq = http.request(
|
|
1375
|
+
{
|
|
1376
|
+
hostname: "127.0.0.1",
|
|
1377
|
+
port: app.port,
|
|
1378
|
+
method: req.method,
|
|
1379
|
+
path: req.url,
|
|
1380
|
+
headers,
|
|
1381
|
+
},
|
|
1382
|
+
(proxyRes) => {
|
|
1383
|
+
if (settled) {
|
|
1384
|
+
proxyRes.resume();
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
settled = true;
|
|
1388
|
+
clearTimeout(responseTimer);
|
|
1389
|
+
app.ready = true;
|
|
1390
|
+
const statusCode = proxyRes.statusCode ?? 502;
|
|
1391
|
+
const responseHeaders = { ...proxyRes.headers };
|
|
1392
|
+
if (statusCode >= 300 && statusCode < 400) {
|
|
1393
|
+
const rewritten = rewriteRedirectLocation(
|
|
1394
|
+
app,
|
|
1395
|
+
firstHeaderValue(responseHeaders.location),
|
|
1396
|
+
);
|
|
1397
|
+
if (rewritten) responseHeaders.location = rewritten;
|
|
1398
|
+
}
|
|
1399
|
+
res.writeHead(statusCode, responseHeaders);
|
|
1400
|
+
proxyRes.once("error", () => {
|
|
1401
|
+
if (!res.destroyed) res.destroy();
|
|
1402
|
+
});
|
|
1403
|
+
proxyRes.pipe(res);
|
|
1404
|
+
},
|
|
1405
|
+
);
|
|
1406
|
+
proxyReq.once("socket", (socket) => {
|
|
1407
|
+
attachGatewaySocketErrorSink(socket);
|
|
1408
|
+
});
|
|
1409
|
+
res.once("error", () => {
|
|
1410
|
+
proxyReq.destroy();
|
|
1411
|
+
});
|
|
1412
|
+
responseTimer = setTimeout(() => {
|
|
1413
|
+
if (settled) return;
|
|
1414
|
+
settled = true;
|
|
1415
|
+
app.ready = false;
|
|
1416
|
+
proxyReq.destroy();
|
|
1417
|
+
ensureReadinessProbe(app);
|
|
1418
|
+
if (res.headersSent) {
|
|
1419
|
+
res.end();
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
if (wantsHtml(req)) {
|
|
1423
|
+
serveStartingPage();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
res.writeHead(504, { "content-type": "text/plain" });
|
|
1427
|
+
res.end(
|
|
1428
|
+
`App "${app.id}" did not return response headers within ${formatProxyReadyTimeout(responseTimeoutMs)}.`,
|
|
1429
|
+
);
|
|
1430
|
+
}, responseTimeoutMs);
|
|
1431
|
+
responseTimer.unref();
|
|
1432
|
+
|
|
1433
|
+
proxyReq.on("error", (err) => {
|
|
1434
|
+
clearTimeout(responseTimer);
|
|
1435
|
+
if (settled) return;
|
|
1436
|
+
settled = true;
|
|
1437
|
+
if (res.headersSent) {
|
|
1438
|
+
res.end();
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
1442
|
+
res.end(`App "${app.id}" is not ready yet: ${err.message}`);
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
req.pipe(proxyReq);
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
// Fast path: the upstream has accepted at least one request before, so
|
|
1449
|
+
// it's listening. Skip the probe so steady-state requests stay zero-latency.
|
|
1450
|
+
if (app.ready && !cold) {
|
|
1451
|
+
dispatch();
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Cold path: hold non-HTML requests open while the child server boots.
|
|
1456
|
+
// Node keeps the request body paused until pipe() attaches.
|
|
1457
|
+
void waitForHttpReady(app, Date.now() + proxyReadyTimeoutMs).then(
|
|
1458
|
+
(ready) => {
|
|
1459
|
+
if (!ready) {
|
|
1460
|
+
failAppStartupTimeout(app);
|
|
1461
|
+
if (!res.headersSent) {
|
|
1462
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
1463
|
+
res.end(
|
|
1464
|
+
`App "${app.id}" is not ready yet: no HTTP response from 127.0.0.1:${app.port}`,
|
|
1465
|
+
);
|
|
1466
|
+
} else {
|
|
1467
|
+
res.end();
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
app.ready = true;
|
|
1472
|
+
dispatch();
|
|
1473
|
+
},
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function proxyUpgrade(
|
|
1478
|
+
app: WorkspaceApp,
|
|
1479
|
+
req: http.IncomingMessage,
|
|
1480
|
+
socket: Duplex,
|
|
1481
|
+
head: Buffer,
|
|
1482
|
+
): void {
|
|
1483
|
+
startApp(app);
|
|
1484
|
+
let target: net.Socket | undefined;
|
|
1485
|
+
attachGatewaySocketErrorSink(socket, () => {
|
|
1486
|
+
target?.destroy();
|
|
1487
|
+
});
|
|
1488
|
+
socket.once("close", () => {
|
|
1489
|
+
target?.destroy();
|
|
1490
|
+
});
|
|
1491
|
+
void waitForPort(app.port, Date.now() + proxyReadyTimeoutMs).then(
|
|
1492
|
+
(ready) => {
|
|
1493
|
+
if (!ready) {
|
|
1494
|
+
failAppStartupTimeout(app);
|
|
1495
|
+
socket.destroy();
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (socket.destroyed) return;
|
|
1499
|
+
app.ready = true;
|
|
1500
|
+
const upstream = net.connect(app.port, "127.0.0.1", () => {
|
|
1501
|
+
const headers = Object.entries(
|
|
1502
|
+
proxyHeaders(req, `127.0.0.1:${app.port}`),
|
|
1503
|
+
)
|
|
1504
|
+
.flatMap(([key, value]) =>
|
|
1505
|
+
Array.isArray(value)
|
|
1506
|
+
? value.map((item) => `${key}: ${item}`)
|
|
1507
|
+
: [`${key}: ${value ?? ""}`],
|
|
1508
|
+
)
|
|
1509
|
+
.join("\r\n");
|
|
1510
|
+
upstream.write(
|
|
1511
|
+
`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n${headers}\r\n\r\n`,
|
|
1512
|
+
);
|
|
1513
|
+
if (head.length) upstream.write(head);
|
|
1514
|
+
socket.pipe(upstream).pipe(socket);
|
|
1515
|
+
});
|
|
1516
|
+
target = upstream;
|
|
1517
|
+
|
|
1518
|
+
attachGatewaySocketErrorSink(upstream, () => {
|
|
1519
|
+
if (!socket.destroyed) socket.destroy();
|
|
1520
|
+
});
|
|
1521
|
+
upstream.once("close", () => {
|
|
1522
|
+
if (!socket.destroyed) socket.destroy();
|
|
1523
|
+
});
|
|
1524
|
+
},
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function handleWatcherError(err: NodeJS.ErrnoException): void {
|
|
1529
|
+
if (isWorkspaceWatcherLimitError(err)) {
|
|
1530
|
+
stderr.write(
|
|
1531
|
+
`[workspace] Recursive file watcher hit the system limit (${err.code}). ` +
|
|
1532
|
+
`New apps will still be detected via polling every ~2s. ` +
|
|
1533
|
+
(err.code === "ENOSPC"
|
|
1534
|
+
? `On Linux you can raise the limit with ` +
|
|
1535
|
+
`\`sudo sysctl fs.inotify.max_user_watches=524288\` ` +
|
|
1536
|
+
`(persist via /etc/sysctl.d/*.conf). `
|
|
1537
|
+
: `Try closing other dev servers or raising your open-file limit. `) +
|
|
1538
|
+
`On macOS/Windows this usually ` +
|
|
1539
|
+
`means too many other watchers are running.\n`,
|
|
1540
|
+
);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (err.code === "ENOENT") {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
stderr.write(
|
|
1547
|
+
`[workspace] Recursive file watcher failed (${err.code ?? "unknown"}): ${err.message}. ` +
|
|
1548
|
+
`Falling back to polling.\n`,
|
|
1549
|
+
);
|
|
1550
|
+
Sentry.captureException(err, {
|
|
1551
|
+
tags: { handled: "dev-watch-unknown" },
|
|
1552
|
+
level: "warning",
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function startWorkspaceProcesses(): void {
|
|
1557
|
+
if (workspaceStarted) return;
|
|
1558
|
+
workspaceStarted = true;
|
|
1559
|
+
for (const id of initialWorkspaceAppIds(
|
|
1560
|
+
apps,
|
|
1561
|
+
defaultApp,
|
|
1562
|
+
eager,
|
|
1563
|
+
redirectRootToDefault,
|
|
1564
|
+
)) {
|
|
1565
|
+
const app = appById.get(id);
|
|
1566
|
+
if (app) startApp(app);
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
const watcher = fs.watch(appsDir, { recursive: true }, scheduleSync);
|
|
1570
|
+
watcher.on("error", (err) => {
|
|
1571
|
+
handleWatcherError(err as NodeJS.ErrnoException);
|
|
1572
|
+
});
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
handleWatcherError(err as NodeJS.ErrnoException);
|
|
1575
|
+
}
|
|
1576
|
+
setInterval(() => {
|
|
1577
|
+
void syncApps().catch(() => {});
|
|
1578
|
+
}, 2_000).unref();
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Background-spawn every app that wasn't started by `startWorkspaceProcesses`.
|
|
1583
|
+
* The lazy proxy still handles correctness (an on-demand request always
|
|
1584
|
+
* starts its target app); this is purely so the first navigation into a
|
|
1585
|
+
* non-default app doesn't pay the cold Vite + esbuild prebundle cost.
|
|
1586
|
+
*
|
|
1587
|
+
* Fires after a short delay so the default app's prebundle gets first dibs
|
|
1588
|
+
* on CPU. Concurrency-limited to avoid hammering a small dev machine —
|
|
1589
|
+
* each Vite spawn briefly maxes out a core during prebundling.
|
|
1590
|
+
*/
|
|
1591
|
+
async function prewarmRemainingApps(): Promise<void> {
|
|
1592
|
+
const concurrency = workspacePrewarmConcurrency(args, env);
|
|
1593
|
+
const delayMs = workspacePrewarmDelayMs(env);
|
|
1594
|
+
|
|
1595
|
+
const queue = apps
|
|
1596
|
+
.filter((app) => app.id !== defaultApp)
|
|
1597
|
+
.filter((app) => !(app.process && !app.process.killed))
|
|
1598
|
+
.map((app) => app.id);
|
|
1599
|
+
|
|
1600
|
+
if (queue.length === 0) return;
|
|
1601
|
+
|
|
1602
|
+
stdout.write(
|
|
1603
|
+
`[workspace] Prewarming ${queue.length} app(s) in the background ` +
|
|
1604
|
+
`(concurrency ${concurrency}; pass --no-prewarm to disable)\n`,
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
if (delayMs > 0) {
|
|
1608
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1609
|
+
}
|
|
1610
|
+
if (shuttingDown) return;
|
|
1611
|
+
|
|
1612
|
+
let next = 0;
|
|
1613
|
+
async function worker(): Promise<void> {
|
|
1614
|
+
while (!shuttingDown && next < queue.length) {
|
|
1615
|
+
const id = queue[next++];
|
|
1616
|
+
const app = appById.get(id);
|
|
1617
|
+
if (!app) continue;
|
|
1618
|
+
// Another path (a real request, a restart, etc.) may have started
|
|
1619
|
+
// this app already — skip without consuming a worker slot needlessly.
|
|
1620
|
+
if (app.process && !app.process.killed) continue;
|
|
1621
|
+
startApp(app);
|
|
1622
|
+
ensureReadinessProbe(app);
|
|
1623
|
+
// Wait for the upstream to answer HTTP before pulling the next
|
|
1624
|
+
// app off the queue. This is what actually limits *concurrent
|
|
1625
|
+
// prebundling* (not just concurrent spawning) and keeps CPU pressure
|
|
1626
|
+
// sane. proxyReadyTimeoutMs caps any single stuck app.
|
|
1627
|
+
await waitForHttpReady(app, Date.now() + proxyReadyTimeoutMs).catch(
|
|
1628
|
+
() => false,
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const workerCount = Math.max(1, Math.min(concurrency, queue.length));
|
|
1634
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function openBrowser(url: string): void {
|
|
1638
|
+
if (options.openBrowser === false || env.WORKSPACE_NO_OPEN === "1") return;
|
|
1639
|
+
const command =
|
|
1640
|
+
process.platform === "darwin"
|
|
1641
|
+
? "open"
|
|
1642
|
+
: process.platform === "win32"
|
|
1643
|
+
? "cmd"
|
|
1644
|
+
: "xdg-open";
|
|
1645
|
+
const openArgs =
|
|
1646
|
+
process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1647
|
+
const child = spawnProcess(command, openArgs, {
|
|
1648
|
+
stdio: "ignore",
|
|
1649
|
+
detached: true,
|
|
1650
|
+
});
|
|
1651
|
+
child.unref();
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const server = http.createServer(async (req, res) => {
|
|
1655
|
+
const parsedUrl = new URL(req.url || "/", "http://workspace.local");
|
|
1656
|
+
const pathname = parsedUrl.pathname;
|
|
1657
|
+
|
|
1658
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
1659
|
+
await syncApps().catch(() => {});
|
|
1660
|
+
const currentDefaultApp =
|
|
1661
|
+
explicitDefaultApp && appById.has(explicitDefaultApp)
|
|
1662
|
+
? explicitDefaultApp
|
|
1663
|
+
: appById.has("dispatch")
|
|
1664
|
+
? "dispatch"
|
|
1665
|
+
: defaultApp;
|
|
1666
|
+
const shouldRedirectRoot =
|
|
1667
|
+
Boolean(explicitDefaultApp && appById.has(explicitDefaultApp)) ||
|
|
1668
|
+
appById.has("dispatch");
|
|
1669
|
+
if (shouldRedirectRoot) {
|
|
1670
|
+
res.writeHead(302, {
|
|
1671
|
+
location: `/${currentDefaultApp}${parsedUrl.search}`,
|
|
1672
|
+
});
|
|
1673
|
+
res.end();
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1677
|
+
res.end(renderIndex(apps));
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (pathname === "/_workspace/apps") {
|
|
1682
|
+
await syncApps().catch(() => {});
|
|
1683
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1684
|
+
res.end(
|
|
1685
|
+
JSON.stringify(
|
|
1686
|
+
apps.map((app) => ({
|
|
1687
|
+
id: app.id,
|
|
1688
|
+
name: app.name,
|
|
1689
|
+
description: app.description,
|
|
1690
|
+
path: `/${app.id}`,
|
|
1691
|
+
audience: app.audience,
|
|
1692
|
+
publicPaths: app.publicPaths,
|
|
1693
|
+
protectedPaths: app.protectedPaths,
|
|
1694
|
+
port: app.port,
|
|
1695
|
+
running: Boolean(app.process && !app.process.killed),
|
|
1696
|
+
})),
|
|
1697
|
+
),
|
|
1698
|
+
);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
let app = appForRequest(req);
|
|
1703
|
+
if (!app) {
|
|
1704
|
+
await syncApps().catch(() => {});
|
|
1705
|
+
app = appForRequest(req);
|
|
1706
|
+
}
|
|
1707
|
+
if (!app) {
|
|
1708
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
|
|
1709
|
+
res.end(renderIndex(apps));
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
proxyHttp(app, req, res);
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
server.on("upgrade", (req, socket, head) => {
|
|
1716
|
+
const app = appForRequest(req);
|
|
1717
|
+
if (!app) {
|
|
1718
|
+
socket.destroy();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
proxyUpgrade(app, req, socket, head);
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
function listen(port: number, attempts = 20): void {
|
|
1725
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
1726
|
+
if (err.code === "EADDRINUSE" && attempts > 0) {
|
|
1727
|
+
listen(port + 1, attempts - 1);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
stderr.write(`[workspace] Could not start gateway: ${err.message}\n`);
|
|
1731
|
+
throw err;
|
|
1732
|
+
});
|
|
1733
|
+
server.listen(port, gatewayHost, () => {
|
|
1734
|
+
const address = server.address();
|
|
1735
|
+
const actualPort =
|
|
1736
|
+
typeof address === "object" && address ? address.port : port;
|
|
1737
|
+
gatewayUrl = `http://${gatewayHost}:${actualPort}`;
|
|
1738
|
+
stdout.write(
|
|
1739
|
+
`[workspace] Default: ${redirectRootToDefault ? `${gatewayUrl}/${defaultApp}` : gatewayUrl}\n`,
|
|
1740
|
+
);
|
|
1741
|
+
stdout.write(`[workspace] Gateway: ${gatewayUrl}\n`);
|
|
1742
|
+
const prewarming = shouldPrewarmWorkspaceApps(args, env);
|
|
1743
|
+
stdout.write(
|
|
1744
|
+
`[workspace] Mode: ${
|
|
1745
|
+
eager ? "eager" : prewarming ? "lazy+prewarm" : "lazy"
|
|
1746
|
+
}${builtMode ? "+built (prebuilt servers; edits promote to vite)" : ""}\n`,
|
|
1747
|
+
);
|
|
1748
|
+
for (const app of apps) {
|
|
1749
|
+
stdout.write(
|
|
1750
|
+
`[workspace] ${app.id}: /${app.id} -> 127.0.0.1:${app.port}\n`,
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
startWorkspaceProcesses();
|
|
1754
|
+
if (prewarming) {
|
|
1755
|
+
void prewarmRemainingApps().catch((err) => {
|
|
1756
|
+
stderr.write(
|
|
1757
|
+
`[workspace] Prewarm error: ${
|
|
1758
|
+
err instanceof Error ? err.message : String(err)
|
|
1759
|
+
}\n`,
|
|
1760
|
+
);
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
openBrowser(
|
|
1764
|
+
redirectRootToDefault ? `${gatewayUrl}/${defaultApp}` : gatewayUrl,
|
|
1765
|
+
);
|
|
1766
|
+
readyResolve({ port: actualPort, url: gatewayUrl });
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function shutdown(): void {
|
|
1771
|
+
if (shuttingDown) return;
|
|
1772
|
+
shuttingDown = true;
|
|
1773
|
+
server.close();
|
|
1774
|
+
for (const app of apps) {
|
|
1775
|
+
app.promoteWatcher?.close();
|
|
1776
|
+
app.promoteWatcher = undefined;
|
|
1777
|
+
killAppProcessTree(app.process);
|
|
1778
|
+
}
|
|
1779
|
+
if (syncTimer) clearTimeout(syncTimer);
|
|
1780
|
+
process.off("SIGINT", handleSigint);
|
|
1781
|
+
process.off("SIGTERM", handleSigterm);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const handleSigint = () => shutdown();
|
|
1785
|
+
const handleSigterm = () => shutdown();
|
|
1786
|
+
process.once("SIGINT", handleSigint);
|
|
1787
|
+
process.once("SIGTERM", handleSigterm);
|
|
1788
|
+
|
|
1789
|
+
listen(requestedPort);
|
|
1790
|
+
|
|
1791
|
+
return {
|
|
1792
|
+
apps,
|
|
1793
|
+
defaultApp,
|
|
1794
|
+
gatewayUrl: () => gatewayUrl,
|
|
1795
|
+
ready,
|
|
1796
|
+
server,
|
|
1797
|
+
shutdown,
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function isDirectRun(): boolean {
|
|
1802
|
+
const entry = process.argv[1];
|
|
1803
|
+
if (!entry) return false;
|
|
1804
|
+
try {
|
|
1805
|
+
return path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
1806
|
+
} catch {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (isDirectRun()) {
|
|
1812
|
+
runWorkspaceDev({ args: process.argv.slice(2) }).catch((err) => {
|
|
1813
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
});
|
|
1816
|
+
}
|