@skgo/sveltekit-adapter 0.18.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +4 -0
- package/package.json +1 -1
- package/skgo-adapter/entry.js +8 -8
- package/skgo-adapter/env.js +4 -0
- package/skgo-adapter/prerender.d.ts +4 -0
- package/skgo-adapter/prerender.js +915 -795
- package/skgo-adapter.js +28 -6
|
@@ -1,833 +1,953 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync } from
|
|
2
|
-
import { createHash, randomUUID } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { createServer, createConnection } from "node:net";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { isMainThread, BroadcastChannel, receiveMessageOnPort } from "node:worker_threads";
|
|
11
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
12
|
|
|
11
|
-
const TAG = 'skgo-prerender-v1';
|
|
12
13
|
const owners = new Map();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const SERVICE_URL = "SKGO_PRERENDER_SERVICE_URL";
|
|
15
|
+
const SERVICE_SECRET = "SKGO_PRERENDER_SERVICE_SECRET";
|
|
16
|
+
const PREDICATE_CHANNEL = "SKGO_PRERENDER_PREDICATE_CHANNEL";
|
|
17
|
+
const CALLBACK_TIMEOUT = 30_000;
|
|
18
|
+
const STARTUP_TIMEOUT = 60_000;
|
|
19
|
+
const SHUTDOWN_GRACE = 150;
|
|
20
|
+
const CLEANUP_TIMEOUT = 5_000;
|
|
21
|
+
const internalFetch = new AsyncLocalStorage();
|
|
22
|
+
const cookieRelays = new WeakMap();
|
|
17
23
|
function appRoot() {
|
|
18
|
-
|
|
24
|
+
return resolve(process.cwd());
|
|
25
|
+
}
|
|
26
|
+
function reportOwnerFailure(error) {
|
|
27
|
+
const cleanup = error?.cleanupError ? `; cleanup failed: ${error.cleanupError.message}` : "";
|
|
28
|
+
process.stderr.write(`skgo prerender owner failure: ${error?.message ?? error}${cleanup}\n`);
|
|
19
29
|
}
|
|
20
|
-
|
|
21
30
|
function kitRootFor(root) {
|
|
22
|
-
|
|
31
|
+
return realpathSync(join(root, "node_modules/@sveltejs/kit"));
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
function kitQueueFailure(detail) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
const error = new Error(
|
|
36
|
+
`SKGO_KIT_PRERENDER_QUEUE: ${detail}. Run "go tool skgo kit-patch --web web --apply" from the Go app root, then "cd web && node_modules/.bin/vp install --no-frozen-lockfile", then "go tool skgo kit-patch --web web --check".`,
|
|
37
|
+
);
|
|
38
|
+
error.name = "SKGO_KIT_PRERENDER_QUEUE";
|
|
39
|
+
error.code = "SKGO_KIT_PRERENDER_QUEUE";
|
|
40
|
+
return error;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
function verifyKitQueue(root) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
let metadata;
|
|
45
|
+
try {
|
|
46
|
+
const appRequire = createRequire(join(root, "package.json"));
|
|
47
|
+
const adapterPackage = appRequire.resolve("@skgo/sveltekit-adapter/package.json");
|
|
48
|
+
metadata = JSON.parse(
|
|
49
|
+
readFileSync(
|
|
50
|
+
join(dirname(adapterPackage), "skgo-adapter/compat/kit-3.0.0-queue.json"),
|
|
51
|
+
"utf8",
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw kitQueueFailure(`adapter compatibility metadata could not be read: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const kitRoot = kitRootFor(root);
|
|
59
|
+
const pkg = JSON.parse(readFileSync(join(kitRoot, "package.json"), "utf8"));
|
|
60
|
+
const queuePath = join(kitRoot, metadata.queuePath);
|
|
61
|
+
const queue = readFileSync(queuePath);
|
|
62
|
+
const digest = createHash("sha256").update(queue).digest("hex");
|
|
63
|
+
if (
|
|
64
|
+
pkg.name !== metadata.package ||
|
|
65
|
+
pkg.version !== metadata.version ||
|
|
66
|
+
digest !== metadata.correctedSHA256
|
|
67
|
+
) {
|
|
68
|
+
throw kitQueueFailure(
|
|
69
|
+
`resolved ${pkg.name ?? "unknown"}@${pkg.version ?? "unknown"} queue SHA-256 ${digest}; expected ${metadata.package}@${metadata.version} with SHA-256 ${metadata.correctedSHA256}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error?.code === "SKGO_KIT_PRERENDER_QUEUE") throw error;
|
|
74
|
+
throw kitQueueFailure(`resolved Kit queue could not be verified: ${error.message}`);
|
|
75
|
+
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
function vitePlusBuildParent(root) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function reportOwnerFailure(error) {
|
|
85
|
-
const primary = error?.message ?? String(error);
|
|
86
|
-
const cleanup = error?.cleanupError ? `; cleanup failed: ${error.cleanupError.message ?? error.cleanupError}` : '';
|
|
87
|
-
try { process.stderr.write(`skgo prerender owner failure: ${primary}${cleanup}\n`); } catch {}
|
|
79
|
+
if (
|
|
80
|
+
!isMainThread ||
|
|
81
|
+
process.env.NODE_PACKAGE_MANAGER !== "vite-plus" ||
|
|
82
|
+
process.argv[2] !== "build" ||
|
|
83
|
+
!process.argv[1]
|
|
84
|
+
)
|
|
85
|
+
return null;
|
|
86
|
+
try {
|
|
87
|
+
const appRequire = createRequire(join(root, "package.json"));
|
|
88
|
+
const vpRequire = createRequire(appRequire.resolve("vite-plus/package.json"));
|
|
89
|
+
const expected = realpathSync(join(dirname(vpRequire.resolve("vite")), "cli.js"));
|
|
90
|
+
return realpathSync(process.argv[1]) === expected && process.ppid > 1 ? process.ppid : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
function validateEnvelope(request, raw) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
97
|
+
let value;
|
|
98
|
+
try {
|
|
99
|
+
value = JSON.parse(raw);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`skgo prerender command returned malformed JSON: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
104
|
+
throw new Error("skgo prerender command returned a non-object response");
|
|
105
|
+
if (["begin", "response", "end", "endpoint", "cookies", "resolve-option"].includes(request.kind)) return;
|
|
106
|
+
if (request.kind === "remote-inputs") {
|
|
107
|
+
if (typeof value.inputs !== "string")
|
|
108
|
+
throw new Error("skgo prerender inputs response has no devalue payload");
|
|
109
|
+
} else if (request.kind === "remote") {
|
|
110
|
+
if (value.type === "result" && typeof value.data === "string") return;
|
|
111
|
+
if (
|
|
112
|
+
value.type === "error" &&
|
|
113
|
+
value.kind === "app" &&
|
|
114
|
+
value.error &&
|
|
115
|
+
Number.isInteger(value.error.status) &&
|
|
116
|
+
typeof value.error.message === "string"
|
|
117
|
+
)
|
|
118
|
+
return;
|
|
119
|
+
if (
|
|
120
|
+
value.type === "error" &&
|
|
121
|
+
value.kind === "unknown" &&
|
|
122
|
+
value.error &&
|
|
123
|
+
Number.isInteger(value.error.status) &&
|
|
124
|
+
typeof value.error.message === "string" &&
|
|
125
|
+
typeof value.diagnostic === "string"
|
|
126
|
+
)
|
|
127
|
+
return;
|
|
128
|
+
if (
|
|
129
|
+
value.type === "redirect" &&
|
|
130
|
+
Number.isInteger(value.redirect?.status) &&
|
|
131
|
+
value.redirect.status >= 300 &&
|
|
132
|
+
value.redirect.status <= 308 &&
|
|
133
|
+
typeof value.redirect.location === "string"
|
|
134
|
+
)
|
|
135
|
+
return;
|
|
136
|
+
throw new Error("skgo prerender remote response has an invalid envelope");
|
|
137
|
+
} else if (request.kind === "load") {
|
|
138
|
+
const allowed = ["data", "chunks", "error", "failure", "redirect", "headers", "cookies"];
|
|
139
|
+
if (!allowed.some((key) => key in value))
|
|
140
|
+
throw new Error("skgo prerender load response has no recognized fields");
|
|
141
|
+
if (
|
|
142
|
+
value.data !== undefined &&
|
|
143
|
+
typeof value.data !== "string" &&
|
|
144
|
+
!(value.data && typeof value.data === "object")
|
|
145
|
+
)
|
|
146
|
+
throw new Error("skgo prerender load response has invalid data");
|
|
147
|
+
if (value.failure !== undefined && typeof value.failure !== "string")
|
|
148
|
+
throw new Error("skgo prerender load response has invalid failure");
|
|
149
|
+
} else {
|
|
150
|
+
throw new Error(`skgo prerender owner received unsupported request kind ${request.kind}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function alive(job) {
|
|
155
|
+
if (!job.child.pid) return false;
|
|
156
|
+
try {
|
|
157
|
+
process.kill(process.platform === "win32" ? job.child.pid : -job.child.pid, 0);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error.code === "ESRCH") return false;
|
|
161
|
+
// EPERM is an existence result for signal 0; actual termination below still
|
|
162
|
+
// reports permission failures rather than mistaking this probe for a drain.
|
|
163
|
+
if (error.code === "EPERM") return true;
|
|
164
|
+
error.message = `skgo process group ${job.child.pid} probe failed: ${error.message}`;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function signalTree(job, force) {
|
|
169
|
+
if (!job.child.pid) return;
|
|
170
|
+
if (process.platform === "win32") {
|
|
171
|
+
await bounded(
|
|
172
|
+
new Promise((resolve, reject) => {
|
|
173
|
+
const args = ["/PID", String(job.child.pid), "/T", ...(force ? ["/F"] : [])];
|
|
174
|
+
const killer = spawn("taskkill", args, { stdio: "ignore", windowsHide: true });
|
|
175
|
+
killer.once("error", reject);
|
|
176
|
+
killer.once("close", resolve);
|
|
177
|
+
}),
|
|
178
|
+
CLEANUP_TIMEOUT,
|
|
179
|
+
"skgo prerender taskkill did not finish",
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
try {
|
|
183
|
+
process.kill(-job.child.pid, force ? "SIGKILL" : "SIGTERM");
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error.code === "ESRCH") return;
|
|
186
|
+
// Darwin can reject signals before Node observes an exited child.
|
|
187
|
+
// Retain the error; only bounded reaping and group absence prove cleanup.
|
|
188
|
+
if (error.code === "EPERM") job.signalError ??= error;
|
|
189
|
+
else throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function delay(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
async function bounded(promise, ms, message) {
|
|
197
|
+
let timer;
|
|
198
|
+
try {
|
|
199
|
+
return await Promise.race([
|
|
200
|
+
promise,
|
|
201
|
+
new Promise((_, reject) => {
|
|
202
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
203
|
+
}),
|
|
204
|
+
]);
|
|
205
|
+
} finally {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function stopJob(job, cooperative = false) {
|
|
210
|
+
if (cooperative) {
|
|
211
|
+
job.control?.end();
|
|
212
|
+
}
|
|
213
|
+
if (cooperative || job.hasExited) {
|
|
214
|
+
// Join the child's close before probing its just-exited process group.
|
|
215
|
+
// Descendants holding inherited output pipes still hit the same fixed grace.
|
|
216
|
+
await bounded(job.closed, SHUTDOWN_GRACE, "shutdown grace elapsed").catch((error) => {
|
|
217
|
+
if (error.message !== "shutdown grace elapsed") throw error;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (alive(job)) {
|
|
221
|
+
await signalTree(job, false);
|
|
222
|
+
await delay(150);
|
|
223
|
+
if (alive(job)) await signalTree(job, true);
|
|
224
|
+
}
|
|
225
|
+
await bounded(
|
|
226
|
+
job.closed,
|
|
227
|
+
CLEANUP_TIMEOUT,
|
|
228
|
+
`skgo prerender owned process did not close${job.signalError ? ": " + job.signalError.message : ""}`,
|
|
229
|
+
);
|
|
230
|
+
const deadline = Date.now() + CLEANUP_TIMEOUT;
|
|
231
|
+
while (alive(job) && Date.now() < deadline) await delay(20);
|
|
232
|
+
if (alive(job))
|
|
233
|
+
throw new Error(`skgo could not drain process group ${job.child.pid}`, {
|
|
234
|
+
cause: job.signalError,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
// Node's generated stdin pipe is write-only at the parent. On Windows, pass
|
|
238
|
+
// a connected duplex pipe as stdin so Go receives its native inherited handle.
|
|
239
|
+
async function windowsControl() {
|
|
240
|
+
const listener = createServer();
|
|
241
|
+
const path = `\\\\.\\pipe\\skgo-prerender-${randomUUID()}`;
|
|
242
|
+
let childEnd, parentEnd;
|
|
243
|
+
const accepted = new Promise((resolve, reject) => {
|
|
244
|
+
listener.once("connection", (socket) => {
|
|
245
|
+
parentEnd = socket;
|
|
246
|
+
resolve(socket);
|
|
247
|
+
});
|
|
248
|
+
listener.once("error", reject);
|
|
249
|
+
});
|
|
250
|
+
accepted.catch(() => {});
|
|
251
|
+
try {
|
|
252
|
+
await bounded(
|
|
253
|
+
new Promise((resolve, reject) => {
|
|
254
|
+
listener.once("error", reject);
|
|
255
|
+
listener.listen(path, resolve);
|
|
256
|
+
}),
|
|
257
|
+
STARTUP_TIMEOUT,
|
|
258
|
+
"skgo prerender control pipe startup timed out",
|
|
259
|
+
);
|
|
260
|
+
childEnd = createConnection(path);
|
|
261
|
+
const connected = new Promise((resolve, reject) => {
|
|
262
|
+
childEnd.once("connect", resolve);
|
|
263
|
+
childEnd.once("error", reject);
|
|
264
|
+
});
|
|
265
|
+
await bounded(
|
|
266
|
+
Promise.all([accepted, connected]),
|
|
267
|
+
STARTUP_TIMEOUT,
|
|
268
|
+
"skgo prerender control pipe startup timed out",
|
|
269
|
+
);
|
|
270
|
+
listener.close();
|
|
271
|
+
return { childEnd, parentEnd };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
childEnd?.destroy();
|
|
274
|
+
parentEnd?.destroy();
|
|
275
|
+
listener.close();
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function startJob(owner, command, args, options = {}) {
|
|
281
|
+
const child = spawn(command, args, {
|
|
282
|
+
cwd: owner.cwd,
|
|
283
|
+
env: options.env ?? process.env,
|
|
284
|
+
stdio: options.service
|
|
285
|
+
? options.control
|
|
286
|
+
? [options.control.childEnd, "pipe", "pipe"]
|
|
287
|
+
: ["ignore", "pipe", "pipe", "pipe"]
|
|
288
|
+
: ["ignore", "pipe", "pipe"],
|
|
289
|
+
detached: process.platform !== "win32",
|
|
290
|
+
windowsHide: true,
|
|
291
|
+
});
|
|
292
|
+
const job = {
|
|
293
|
+
child,
|
|
294
|
+
stderr: "",
|
|
295
|
+
control: options.service ? (options.control?.parentEnd ?? child.stdio[3]) : null,
|
|
296
|
+
};
|
|
297
|
+
options.control?.childEnd.destroy();
|
|
298
|
+
owner.jobs.add(job);
|
|
299
|
+
job.exited = new Promise((resolve, reject) => {
|
|
300
|
+
child.once("error", reject);
|
|
301
|
+
child.once("exit", (code, signal) => {
|
|
302
|
+
job.hasExited = true;
|
|
303
|
+
resolve({ code, signal });
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
job.closed = new Promise((resolve) => {
|
|
307
|
+
child.once("close", resolve);
|
|
308
|
+
});
|
|
309
|
+
job.exited.catch(() => {});
|
|
310
|
+
child.stdout.on("data", (chunk) => {
|
|
311
|
+
if (options.service) process.stdout.write(chunk);
|
|
312
|
+
});
|
|
313
|
+
child.stderr.on("data", (chunk) => {
|
|
314
|
+
if (options.service) process.stderr.write(chunk);
|
|
315
|
+
else job.stderr += chunk;
|
|
316
|
+
});
|
|
317
|
+
job.control?.on("error", () => {});
|
|
318
|
+
// Descendants can inherit stdout/stderr and hold close open after the leader exits.
|
|
319
|
+
child.once("exit", () => {
|
|
320
|
+
void signalTree(job, false).catch(reportOwnerFailure);
|
|
321
|
+
});
|
|
322
|
+
return job;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The main build thread owns its compiler, HTTP service and scratch directory. */
|
|
326
|
+
export function createPrerenderOwner(root = appRoot(), prerenderPackage) {
|
|
327
|
+
if (!isMainThread) return null;
|
|
328
|
+
root = resolve(root);
|
|
329
|
+
if (owners.has(root)) return owners.get(root);
|
|
330
|
+
const manifest = JSON.parse(readFileSync(join(root, "skgo.remotes.json"), "utf8"));
|
|
331
|
+
// The generator emits this entry only when loads or prerender remotes exist.
|
|
332
|
+
if (!manifest.prerender) return null;
|
|
333
|
+
const owner = {
|
|
334
|
+
root,
|
|
335
|
+
cwd: resolve(root, manifest.prerender.root),
|
|
336
|
+
pkg: prerenderPackage ?? manifest.prerender.package,
|
|
337
|
+
jobs: new Set(),
|
|
338
|
+
closing: false,
|
|
339
|
+
failed: null,
|
|
340
|
+
dir: null,
|
|
341
|
+
service: null,
|
|
342
|
+
previousEnv: null,
|
|
343
|
+
signals: new Map(),
|
|
344
|
+
pendingPredicates: new Map(),
|
|
345
|
+
cleanupPromise: null,
|
|
346
|
+
preparePromise: null,
|
|
347
|
+
fail(error) {
|
|
348
|
+
owner.failed ??= error;
|
|
349
|
+
for (const [id, cell] of owner.pendingPredicates) {
|
|
350
|
+
owner.channel?.postMessage({id, error: owner.failed.stack ?? owner.failed.message ?? String(owner.failed)});
|
|
351
|
+
Atomics.store(cell, 0, 1);
|
|
352
|
+
Atomics.notify(cell, 0);
|
|
353
|
+
}
|
|
354
|
+
owner.pendingPredicates.clear();
|
|
355
|
+
return owner.cleanup().catch((cleanupError) => {
|
|
356
|
+
owner.cleanupFailure = cleanupError;
|
|
357
|
+
if (typeof owner.failed === "object" && owner.failed !== cleanupError) {
|
|
358
|
+
if (!owner.failed.cleanupError) {
|
|
359
|
+
const detail =
|
|
360
|
+
cleanupError.errors?.map((error) => error.message).join("; ") ?? cleanupError.message;
|
|
361
|
+
const diagnostic = `cleanup failed: ${detail}`;
|
|
362
|
+
owner.failed.message += `; ${diagnostic}`;
|
|
363
|
+
if (owner.failed.stack) owner.failed.stack += `\n${diagnostic}`;
|
|
364
|
+
}
|
|
365
|
+
owner.failed.cleanupError = cleanupError;
|
|
366
|
+
}
|
|
367
|
+
throw owner.failed;
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
prepare() {
|
|
371
|
+
return (owner.preparePromise ??= (async () => {
|
|
372
|
+
owner.dir = mkdtempSync(join(tmpdir(), "skgo-prerender-"));
|
|
373
|
+
mkdirSync(join(owner.dir, "go-tmp"));
|
|
374
|
+
const binary = join(owner.dir, process.platform === "win32" ? "build.exe" : "build");
|
|
375
|
+
try {
|
|
376
|
+
const compiler = startJob(owner, "go", ["build", "-o", binary, owner.pkg], {
|
|
377
|
+
env: { ...process.env, GOTMPDIR: join(owner.dir, "go-tmp") },
|
|
378
|
+
});
|
|
379
|
+
const result = await compiler.exited;
|
|
380
|
+
await stopJob(compiler);
|
|
381
|
+
owner.jobs.delete(compiler);
|
|
382
|
+
if (result.code !== 0)
|
|
383
|
+
throw new Error(`skgo prerender compiler failed: ${compiler.stderr.trim()}`);
|
|
384
|
+
if (owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
385
|
+
const control = process.platform === "win32" ? await windowsControl() : null;
|
|
386
|
+
if (owner.closing) {
|
|
387
|
+
control?.childEnd.destroy();
|
|
388
|
+
control?.parentEnd.destroy();
|
|
389
|
+
throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
390
|
+
}
|
|
391
|
+
let service;
|
|
392
|
+
try {
|
|
393
|
+
service = owner.service = startJob(owner, binary, [], { service: true, control });
|
|
394
|
+
} catch (error) {
|
|
395
|
+
control?.childEnd.destroy();
|
|
396
|
+
control?.parentEnd.destroy();
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
const ready = new Promise((resolve, reject) => {
|
|
400
|
+
let data = "";
|
|
401
|
+
const receive = (chunk) => {
|
|
402
|
+
data += chunk;
|
|
403
|
+
if (!data.includes("\n")) return;
|
|
404
|
+
service.control.off("data", receive);
|
|
405
|
+
try {
|
|
406
|
+
const value = JSON.parse(data.slice(0, data.indexOf("\n")));
|
|
407
|
+
const url = new URL(value.url);
|
|
408
|
+
if (
|
|
409
|
+
url.protocol !== "http:" ||
|
|
410
|
+
url.hostname !== "127.0.0.1" ||
|
|
411
|
+
!url.port ||
|
|
412
|
+
typeof value.secret !== "string" ||
|
|
413
|
+
!value.secret
|
|
414
|
+
)
|
|
415
|
+
throw new Error("invalid readiness");
|
|
416
|
+
resolve(value);
|
|
417
|
+
} catch {
|
|
418
|
+
reject(new Error("skgo prerender service returned invalid readiness"));
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
service.control.setEncoding("utf8");
|
|
422
|
+
service.control.on("data", receive);
|
|
423
|
+
service.control.once("error", () =>
|
|
424
|
+
reject(new Error("skgo prerender service control channel failed")),
|
|
425
|
+
);
|
|
426
|
+
service.exited.then(
|
|
427
|
+
({ code, signal }) =>
|
|
428
|
+
reject(
|
|
429
|
+
new Error(`skgo prerender service exited before readiness: ${code ?? signal}`),
|
|
430
|
+
),
|
|
431
|
+
reject,
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
owner.connection = await bounded(
|
|
435
|
+
ready,
|
|
436
|
+
STARTUP_TIMEOUT,
|
|
437
|
+
"skgo prerender service startup timed out",
|
|
438
|
+
);
|
|
439
|
+
process.stderr.write(`skgo prerender service started (pid ${service.child.pid})\n`);
|
|
440
|
+
service.exited.then(
|
|
441
|
+
({ code, signal }) => {
|
|
442
|
+
if (!owner.closing || code !== 0) {
|
|
443
|
+
const error = new Error(
|
|
444
|
+
`skgo prerender service exited unexpectedly: ${code ?? signal}`,
|
|
445
|
+
);
|
|
446
|
+
owner.failed ??= error;
|
|
447
|
+
if (!owner.closing) void owner.fail(error).catch(reportOwnerFailure);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
(error) => {
|
|
451
|
+
if (!owner.closing) void owner.fail(error).catch(reportOwnerFailure);
|
|
452
|
+
},
|
|
453
|
+
);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
await owner.fail(error);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
})());
|
|
459
|
+
},
|
|
460
|
+
publish() {
|
|
461
|
+
if (owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
462
|
+
if (owner.previousEnv) return;
|
|
463
|
+
owner.channel = new BroadcastChannel(`skgo-prerender-${randomUUID()}`);
|
|
464
|
+
owner.channel.unref();
|
|
465
|
+
owner.channel.onmessage = async ({ data }) => {
|
|
466
|
+
if (data?.type === "failure") {
|
|
467
|
+
void owner.fail(new Error(data.error)).catch(reportOwnerFailure);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (data?.type !== "predicate") return;
|
|
471
|
+
const { id, cell, request } = data;
|
|
472
|
+
owner.pendingPredicates.set(id, cell);
|
|
473
|
+
let reply;
|
|
474
|
+
try {
|
|
475
|
+
if (owner.failed || owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
476
|
+
const answer = JSON.parse(await invokeService({ ...request, kind: "resolve-option" }));
|
|
477
|
+
if (typeof answer.value !== "boolean") throw new Error("skgo prerender predicate returned no boolean");
|
|
478
|
+
reply = { id, value: answer.value };
|
|
479
|
+
} catch (error) {
|
|
480
|
+
reply = { id, error: error.stack ?? error.message ?? String(error) };
|
|
481
|
+
}
|
|
482
|
+
if (!owner.pendingPredicates.delete(id)) return;
|
|
483
|
+
// Wake before fail() can wait for the worker/service to stop.
|
|
484
|
+
owner.channel?.postMessage(reply);
|
|
485
|
+
Atomics.store(cell, 0, 1);
|
|
486
|
+
Atomics.notify(cell, 0);
|
|
487
|
+
if (reply.error) void owner.fail(new Error(reply.error)).catch(reportOwnerFailure);
|
|
488
|
+
};
|
|
489
|
+
owner.previousEnv = new Map([
|
|
490
|
+
[SERVICE_URL, process.env[SERVICE_URL]],
|
|
491
|
+
[SERVICE_SECRET, process.env[SERVICE_SECRET]],
|
|
492
|
+
[PREDICATE_CHANNEL, process.env[PREDICATE_CHANNEL]],
|
|
493
|
+
]);
|
|
494
|
+
process.env[SERVICE_URL] = owner.connection.url;
|
|
495
|
+
process.env[SERVICE_SECRET] = owner.connection.secret;
|
|
496
|
+
process.env[PREDICATE_CHANNEL] = owner.channel.name;
|
|
497
|
+
},
|
|
498
|
+
cleanup() {
|
|
499
|
+
if (owner.cleanupPromise) return owner.cleanupPromise;
|
|
500
|
+
owner.closing = true;
|
|
501
|
+
owner.cleanupPromise = (async () => {
|
|
502
|
+
const errors = [];
|
|
503
|
+
owner.channel?.close();
|
|
504
|
+
owner.channel = null;
|
|
505
|
+
for (const job of owner.jobs) {
|
|
506
|
+
try {
|
|
507
|
+
await stopJob(job, job === owner.service);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
errors.push(error);
|
|
510
|
+
} finally {
|
|
511
|
+
job.control?.destroy();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (owner.previousEnv)
|
|
515
|
+
for (const [key, value] of owner.previousEnv) {
|
|
516
|
+
if (value === undefined) delete process.env[key];
|
|
517
|
+
else process.env[key] = value;
|
|
518
|
+
}
|
|
519
|
+
for (const [signal, handler] of owner.signals) process.off(signal, handler);
|
|
520
|
+
if (owner.parentObserver) clearInterval(owner.parentObserver);
|
|
521
|
+
process.off("exit", owner.exitHandler);
|
|
522
|
+
if (!errors.length && owner.dir) rmSync(owner.dir, { recursive: true, force: true });
|
|
523
|
+
owners.delete(root);
|
|
524
|
+
if (owner.service && !errors.length)
|
|
525
|
+
process.stderr.write(`skgo prerender service stopped (pid ${owner.service.child.pid})\n`);
|
|
526
|
+
if (errors.length)
|
|
527
|
+
throw new AggregateError(errors, "skgo could not drain owned prerender processes");
|
|
528
|
+
})();
|
|
529
|
+
return owner.cleanupPromise;
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
owner.exitHandler = () => {
|
|
533
|
+
for (const job of owner.jobs) {
|
|
534
|
+
if (process.platform !== "win32") {
|
|
535
|
+
try {
|
|
536
|
+
process.kill(-job.child.pid, "SIGKILL");
|
|
537
|
+
} catch {}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
process.once("exit", owner.exitHandler);
|
|
542
|
+
const interrupted = async (error, signal) => {
|
|
543
|
+
try {
|
|
544
|
+
await owner.fail(error);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
reportOwnerFailure(error);
|
|
547
|
+
}
|
|
548
|
+
process.exitCode = 1;
|
|
549
|
+
try {
|
|
550
|
+
process.kill(process.pid, signal);
|
|
551
|
+
} catch {
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
556
|
+
const handler = () => {
|
|
557
|
+
void interrupted(new Error(`skgo prerender build interrupted by ${signal}`), signal);
|
|
558
|
+
};
|
|
559
|
+
owner.signals.set(signal, handler);
|
|
560
|
+
process.on(signal, handler);
|
|
561
|
+
}
|
|
562
|
+
const parent = vitePlusBuildParent(root);
|
|
563
|
+
if (parent !== null) {
|
|
564
|
+
owner.parentObserver = setInterval(() => {
|
|
565
|
+
if (process.ppid === parent) return;
|
|
566
|
+
clearInterval(owner.parentObserver);
|
|
567
|
+
void interrupted(new Error("skgo prerender Vite build parent exited"), "SIGTERM");
|
|
568
|
+
}, 50);
|
|
569
|
+
owner.parentObserver.unref();
|
|
570
|
+
}
|
|
571
|
+
owners.set(root, owner);
|
|
572
|
+
return owner;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Vite 8's native buildApp has no failure-finally and its CLI exits immediately.
|
|
576
|
+
* Wrap only those whole-app hooks so their rejection waits for the owner. */
|
|
556
577
|
export function installPrerenderFailureBoundary(config, owner) {
|
|
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
|
-
setHook(plugin, Object.create(Object.getPrototypeOf(hook), descriptors));
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const builder = config.builder;
|
|
617
|
-
if (builder && typeof builder === 'object' && typeof builder.buildApp === 'function') {
|
|
618
|
-
setHook(builder, wrap(builder.buildApp));
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/** Wait for cleanup only when a native failure or signal has begun draining. */
|
|
623
|
-
export async function joinFailedPrerenderOwner(owner) {
|
|
624
|
-
if (owner?.closing) {
|
|
625
|
-
try { await owner.cleanup(); }
|
|
626
|
-
catch (cleanupError) {
|
|
627
|
-
if (owner.failed && typeof owner.failed === 'object') owner.failed.cleanupError = cleanupError;
|
|
628
|
-
throw owner.failed ?? cleanupError;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function receiveWorkerMessage(message) {
|
|
634
|
-
if (!message || message.tag !== TAG || !message.requestId) return;
|
|
635
|
-
const waiter = requestWaiters.get(message.requestId);
|
|
636
|
-
if (!waiter) return;
|
|
637
|
-
if (message.type === 'connected') waiter.resolve(message.session);
|
|
638
|
-
else if (message.type === 'result') waiter.resolve(message.raw);
|
|
639
|
-
else if (message.type === 'failed') waiter.reject(makeError(message.error ?? 'prerender owner failed'));
|
|
640
|
-
else return;
|
|
641
|
-
requestWaiters.delete(message.requestId);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function ensureWorkerListener() {
|
|
645
|
-
if (parentPortListening || !parentPort) return;
|
|
646
|
-
parentPortListening = true;
|
|
647
|
-
parentPort.on('message', receiveWorkerMessage);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function workerRequest(message) {
|
|
651
|
-
if (!parentPort || isMainThread) return Promise.reject(makeError('prerender callbacks require a registered Kit worker'));
|
|
652
|
-
ensureWorkerListener();
|
|
653
|
-
return new Promise((resolve, reject) => {
|
|
654
|
-
const timer = message.type === 'connect' ? setTimeout(() => {
|
|
655
|
-
requestWaiters.delete(message.requestId);
|
|
656
|
-
reject(makeError('skgo prerender owner is unavailable or did not answer the worker handshake'));
|
|
657
|
-
}, 5000) : null;
|
|
658
|
-
requestWaiters.set(message.requestId, {
|
|
659
|
-
resolve: (value) => { if (timer) clearTimeout(timer); resolve(value); },
|
|
660
|
-
reject: (error) => { if (timer) clearTimeout(timer); reject(error); }
|
|
661
|
-
});
|
|
662
|
-
try { parentPort.postMessage({ tag: TAG, ...message }); }
|
|
663
|
-
catch (error) {
|
|
664
|
-
if (timer) clearTimeout(timer);
|
|
665
|
-
requestWaiters.delete(message.requestId);
|
|
666
|
-
reject(error);
|
|
667
|
-
}
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async function workerSession(root) {
|
|
672
|
-
const requestId = randomUUID();
|
|
673
|
-
const session = await workerRequest({ type: 'connect', appRoot: root, requestId });
|
|
674
|
-
if (typeof session !== 'string' || !session) throw makeError('skgo prerender owner handshake returned no session');
|
|
675
|
-
parentPort.postMessage({ tag: TAG, type: 'connected-ack', appRoot: root, session, requestId });
|
|
676
|
-
return session;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function invokeWorker(request) {
|
|
680
|
-
const root = appRoot();
|
|
681
|
-
const session = await workerSession(root);
|
|
682
|
-
const requestId = randomUUID();
|
|
683
|
-
return workerRequest({ type: 'invoke', appRoot: root, session, requestId, request });
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async function reportWorkerFatal(error) {
|
|
687
|
-
const root = appRoot();
|
|
688
|
-
let session;
|
|
689
|
-
try { session = await workerSession(root); }
|
|
690
|
-
catch { throw error; }
|
|
691
|
-
const requestId = randomUUID();
|
|
692
|
-
try {
|
|
693
|
-
await workerRequest({
|
|
694
|
-
type: 'fatal', appRoot: root, session, requestId,
|
|
695
|
-
error: error instanceof Error ? error.message : String(error)
|
|
696
|
-
});
|
|
697
|
-
} catch {
|
|
698
|
-
// The owner replies only after it has drained. Preserve the original
|
|
699
|
-
// worker-side failure as the exception Kit sees.
|
|
700
|
-
}
|
|
701
|
-
throw error;
|
|
578
|
+
if (!owner || typeof config.getSortedPlugins !== "function") return;
|
|
579
|
+
const wrapped = (owner.wrapped ??= new WeakSet());
|
|
580
|
+
const wrap = (handler) => {
|
|
581
|
+
if (wrapped.has(handler)) return handler;
|
|
582
|
+
const replacement = async function (...args) {
|
|
583
|
+
try {
|
|
584
|
+
const result = await handler.apply(this, args);
|
|
585
|
+
if (owner.failed) throw owner.failed;
|
|
586
|
+
return result;
|
|
587
|
+
} catch (error) {
|
|
588
|
+
await owner.fail(error);
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
wrapped.add(replacement);
|
|
593
|
+
return replacement;
|
|
594
|
+
};
|
|
595
|
+
for (const plugin of config.getSortedPlugins("buildApp")) {
|
|
596
|
+
const hook = plugin.buildApp;
|
|
597
|
+
if (typeof hook === "function") plugin.buildApp = wrap(hook);
|
|
598
|
+
else if (hook?.handler) plugin.buildApp = { ...hook, handler: wrap(hook.handler) };
|
|
599
|
+
}
|
|
600
|
+
if (typeof config.builder?.buildApp === "function")
|
|
601
|
+
config.builder.buildApp = wrap(config.builder.buildApp);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function invokeService(request) {
|
|
605
|
+
const url = process.env[SERVICE_URL];
|
|
606
|
+
const secret = process.env[SERVICE_SECRET];
|
|
607
|
+
const identity = request.kind === "resolve-option" ? `${request.option} resolve callback` : `${request.kind} ${request.module}/${request.name ?? ""}`;
|
|
608
|
+
if (!url || !secret) throw new Error(`skgo prerender service unavailable for ${identity}`);
|
|
609
|
+
const { kind, ...body } = request;
|
|
610
|
+
const path = { load: "/load", remote: "/remote", "remote-inputs": "/inputs", begin: "/begin", response: "/response", end: "/end", endpoint: "/endpoint", cookies: "/cookies", "resolve-option": "/resolve-option" }[kind];
|
|
611
|
+
const controller = new AbortController();
|
|
612
|
+
const timer = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT);
|
|
613
|
+
try {
|
|
614
|
+
const response = await fetch(url + path, {
|
|
615
|
+
method: "POST",
|
|
616
|
+
headers: { authorization: `Bearer ${secret}`, "content-type": "application/json" },
|
|
617
|
+
body: JSON.stringify(body),
|
|
618
|
+
signal: controller.signal,
|
|
619
|
+
});
|
|
620
|
+
const raw = await response.text();
|
|
621
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
622
|
+
validateEnvelope(request, raw);
|
|
623
|
+
return raw;
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (controller.signal.aborted) {
|
|
626
|
+
const message = `skgo prerender ${identity} timed out after ${CALLBACK_TIMEOUT}ms`;
|
|
627
|
+
process.stderr.write(`${message}\n`);
|
|
628
|
+
throw new Error(message);
|
|
629
|
+
}
|
|
630
|
+
throw new Error(`skgo prerender ${identity} failed: ${error.message}`);
|
|
631
|
+
} finally {
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
}
|
|
702
634
|
}
|
|
703
635
|
|
|
704
636
|
async function kitRuntime(root) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
637
|
+
const kitRoot = kitRootFor(root);
|
|
638
|
+
const transportURL = pathToFileURL(join(kitRoot, "src/runtime/app/internal/transport.js")).href;
|
|
639
|
+
const internalURL = pathToFileURL(join(kitRoot, "src/exports/internal/shared.js")).href;
|
|
640
|
+
const transport = await import(transportURL);
|
|
641
|
+
const hooks = join(root, ".svelte-kit/output/server/entries/hooks.universal.js");
|
|
642
|
+
let definitions = {};
|
|
643
|
+
try {
|
|
644
|
+
({ transport: definitions = {} } = await import(pathToFileURL(hooks).href));
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
647
|
+
}
|
|
648
|
+
transport.init_transport(definitions);
|
|
649
|
+
const internal = await import(internalURL);
|
|
650
|
+
const require = createRequire(join(kitRoot, "package.json"));
|
|
651
|
+
const devalue = await import(pathToFileURL(require.resolve("devalue")).href);
|
|
652
|
+
return { transport, internal, devalue };
|
|
720
653
|
}
|
|
721
654
|
|
|
722
655
|
let runtimePromise;
|
|
723
656
|
function runtime() {
|
|
724
|
-
|
|
725
|
-
|
|
657
|
+
if (!runtimePromise) runtimePromise = kitRuntime(appRoot());
|
|
658
|
+
return runtimePromise;
|
|
726
659
|
}
|
|
727
660
|
|
|
728
661
|
/** Called by the generated native Kit input producer. */
|
|
729
662
|
export async function remoteInputs(module, name) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (!Array.isArray(values)) throw makeError('skgo prerender inputs response did not decode to an array');
|
|
739
|
-
return values;
|
|
740
|
-
} catch (error) {
|
|
741
|
-
return reportWorkerFatal(error);
|
|
742
|
-
}
|
|
663
|
+
verifyKitQueue(appRoot());
|
|
664
|
+
const raw = await invokeService({ kind: "remote-inputs", module, name });
|
|
665
|
+
const answer = JSON.parse(raw);
|
|
666
|
+
const { transport } = await runtime();
|
|
667
|
+
const values = transport.parse(answer.inputs);
|
|
668
|
+
if (!Array.isArray(values))
|
|
669
|
+
throw new Error("skgo prerender inputs response did not decode to an array");
|
|
670
|
+
return values;
|
|
743
671
|
}
|
|
744
672
|
|
|
745
673
|
/** The emulator callback for a Go load, executing entirely in its Kit worker. */
|
|
746
674
|
export async function remoteLoad(module, source, event) {
|
|
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
|
-
|
|
675
|
+
const kit = await runtime();
|
|
676
|
+
const { devalue, transport } = kit;
|
|
677
|
+
const headers = Object.fromEntries(
|
|
678
|
+
[...event.request.headers].map(([name, value]) => [name, [value]]),
|
|
679
|
+
);
|
|
680
|
+
const cookies = event.cookies.getAll();
|
|
681
|
+
if (cookies.length) {
|
|
682
|
+
headers.cookie = [
|
|
683
|
+
cookies
|
|
684
|
+
.map(({ name, value }) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
|
685
|
+
.join("; "),
|
|
686
|
+
];
|
|
687
|
+
}
|
|
688
|
+
// Kit's JS matcher results cannot construct named Go values. Send its
|
|
689
|
+
// actual route metadata and the unconverted path to the build-only Go load.
|
|
690
|
+
const { manifest } = await import(
|
|
691
|
+
pathToFileURL(join(appRoot(), ".svelte-kit/output/server/manifest-full.js")).href
|
|
692
|
+
);
|
|
693
|
+
const route = manifest.routes.find((candidate) => candidate.id === event.route.id);
|
|
694
|
+
const prefix = manifest.app_path.slice(0, -manifest.app_dir.length);
|
|
695
|
+
const base = prefix ? "/" + prefix.slice(0, -1) : "";
|
|
696
|
+
const { decode_pathname } = await import(
|
|
697
|
+
pathToFileURL(join(kitRootFor(appRoot()), "src/utils/url.js")).href
|
|
698
|
+
);
|
|
699
|
+
const request = {
|
|
700
|
+
kind: "load",
|
|
701
|
+
module,
|
|
702
|
+
handle: event.platform?.skgoRequestHandle,
|
|
703
|
+
url: event.url.href,
|
|
704
|
+
routeId: event.route.id,
|
|
705
|
+
params: {},
|
|
706
|
+
parent: await event.parent(),
|
|
707
|
+
headers,
|
|
708
|
+
...(route
|
|
709
|
+
? {
|
|
710
|
+
routePattern: route.pattern.source,
|
|
711
|
+
routeParams: route.params,
|
|
712
|
+
routePath: decode_pathname(event.url.pathname).slice(base.length) || "/",
|
|
713
|
+
}
|
|
714
|
+
: {}),
|
|
715
|
+
};
|
|
716
|
+
const raw = await invokeService(request);
|
|
717
|
+
const answer = JSON.parse(raw);
|
|
718
|
+
if (typeof answer.failure === "string") {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`skgo: Go load failed during prerender: route ID ${event.route.id}, path ${event.url.pathname}, source ${source}: ${answer.failure}`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
applyRequestEffects(event, answer);
|
|
724
|
+
if (answer.data) {
|
|
725
|
+
const pending = new Map();
|
|
726
|
+
const revivers = {
|
|
727
|
+
...transport.decoders,
|
|
728
|
+
Promise: (id) => {
|
|
729
|
+
let resolve;
|
|
730
|
+
let reject;
|
|
731
|
+
const promise = new Promise((yes, no) => {
|
|
732
|
+
resolve = yes;
|
|
733
|
+
reject = no;
|
|
734
|
+
});
|
|
735
|
+
pending.set(id, { promise, resolve, reject });
|
|
736
|
+
return promise;
|
|
737
|
+
},
|
|
738
|
+
};
|
|
739
|
+
answer.data = devalue.unflatten(answer.data, revivers);
|
|
740
|
+
for (const chunk of answer.chunks ?? []) {
|
|
741
|
+
const deferred = pending.get(chunk.id);
|
|
742
|
+
if (!deferred) throw new Error(`skgo: unknown prerender deferred id ${chunk.id}`);
|
|
743
|
+
if (chunk.error) deferred.reject(new Error(chunk.error));
|
|
744
|
+
else deferred.resolve(devalue.unflatten(chunk.data, revivers));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return answer;
|
|
806
748
|
}
|
|
807
749
|
|
|
808
750
|
/** The emulator callback for a Go prerender remote, executing in Kit's worker. */
|
|
809
751
|
export async function remoteFunction(module, name, arg, event) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
752
|
+
const kit = await runtime();
|
|
753
|
+
const { internal, transport } = kit;
|
|
754
|
+
const payload =
|
|
755
|
+
arg === undefined ? "" : Buffer.from(transport.stringify(arg), "utf8").toString("base64url");
|
|
756
|
+
const headers = Object.fromEntries(
|
|
757
|
+
[...event.request.headers].map(([key, value]) => [key, [value]]),
|
|
758
|
+
);
|
|
759
|
+
const raw = await invokeService({
|
|
760
|
+
kind: "remote",
|
|
761
|
+
module,
|
|
762
|
+
name,
|
|
763
|
+
handle: event.platform?.skgoRequestHandle,
|
|
764
|
+
payload,
|
|
765
|
+
url: event.url.href,
|
|
766
|
+
headers,
|
|
767
|
+
});
|
|
768
|
+
const answer = JSON.parse(raw);
|
|
769
|
+
if (answer.type === "error" && answer.kind === "app") throw new internal.HttpError(answer.error);
|
|
770
|
+
if (answer.type === "error" && answer.kind === "unknown") throw new Error(answer.diagnostic);
|
|
771
|
+
if (answer.type === "redirect")
|
|
772
|
+
throw new internal.Redirect(answer.redirect.status, answer.redirect.location);
|
|
773
|
+
if (answer.type !== "result" || typeof answer.data !== "string")
|
|
774
|
+
throw new Error("skgo prerender remote response is malformed");
|
|
775
|
+
const result = transport.parse(answer.data);
|
|
776
|
+
if (!result || typeof result !== "object" || !Object.hasOwn(result, "_")) {
|
|
777
|
+
throw new Error("skgo prerender remote result omitted its value member");
|
|
778
|
+
}
|
|
779
|
+
return result._;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
function applyRequestEffects(event, answer) {
|
|
784
|
+
for (const [name, values] of Object.entries(answer.headers ?? {}))
|
|
785
|
+
event.setHeaders({ [name]: values.join(", ") });
|
|
786
|
+
for (const cookie of answer.cookies ?? [])
|
|
787
|
+
event.cookies.set(cookie.name, cookie.value, {
|
|
788
|
+
path: cookie.path, ...(cookie.domain ? { domain: cookie.domain } : {}),
|
|
789
|
+
...(cookie.maxAge !== undefined ? { maxAge: cookie.maxAge } : {}), httpOnly: cookie.httpOnly,
|
|
790
|
+
...(cookie.expires ? { expires: new Date(cookie.expires) } : {}),
|
|
791
|
+
...(cookie.partitioned ? { partitioned: true } : {}),
|
|
792
|
+
...(cookie.priority ? { priority: cookie.priority } : {}),
|
|
793
|
+
...(cookie.raw ? { encode: (value) => value } : {}),
|
|
794
|
+
secure: cookie.secure, sameSite: [false, false, "lax", "strict", "none"][cookie.sameSite] ?? false,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Generated hooks.server calls this only while Kit is building. */
|
|
799
|
+
export async function requestHandle(event, resolveRequest) {
|
|
800
|
+
// handleFetch's supplied fetch reaches this hook only for Kit subrequests.
|
|
801
|
+
const fetching = internalFetch.getStore();
|
|
802
|
+
if (fetching) fetching.internal = true;
|
|
803
|
+
const { manifest } = await import(pathToFileURL(join(appRoot(), ".svelte-kit/output/server/manifest-full.js")).href);
|
|
804
|
+
const prefix = manifest.app_path.slice(0, -manifest.app_dir.length);
|
|
805
|
+
const base = prefix ? "/" + prefix.slice(0, -1) : "";
|
|
806
|
+
const routes = await Promise.all(manifest.routes.map(async (route) => ({
|
|
807
|
+
id: route.id, pattern: route.pattern.source, params: route.params,
|
|
808
|
+
...(route.page ? { page: route.page } : {}),
|
|
809
|
+
...(route.endpoint ? { endpoint: { methods: Object.keys(await route.endpoint()).filter((key) => ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "QUERY", "fallback"].includes(key)).map((key) => key === "fallback" ? "*" : key) } } : {}),
|
|
810
|
+
})));
|
|
811
|
+
const route = routes.find((candidate) => candidate.id === event.route.id);
|
|
812
|
+
const { decode_pathname } = await import(pathToFileURL(join(kitRootFor(appRoot()), "src/utils/url.js")).href);
|
|
813
|
+
const answer = JSON.parse(await invokeService({
|
|
814
|
+
kind: "begin", method: event.request.method, url: event.request.url, logicalUrl: event.url.href,
|
|
815
|
+
headers: Object.fromEntries([...event.request.headers].map(([key, value]) => [key, [value]])),
|
|
816
|
+
body: event.request.method === "GET" || event.request.method === "HEAD" ? "" : Buffer.from(await event.request.clone().arrayBuffer()).toString("base64"),
|
|
817
|
+
routeId: event.route.id ?? "", routePattern: route?.pattern ?? "", routeParams: route?.params ?? [],
|
|
818
|
+
routePath: decode_pathname(event.url.pathname).slice(base.length) || "/",
|
|
819
|
+
isSubRequest: event.isSubRequest,
|
|
820
|
+
manifest: { appDir: manifest.app_dir, base, routes },
|
|
821
|
+
}));
|
|
822
|
+
const handle = answer.handle;
|
|
823
|
+
if (!handle) throw new Error("skgo prerender begin omitted request handle");
|
|
824
|
+
let ended = false;
|
|
825
|
+
const end = async () => {
|
|
826
|
+
if (ended) return;
|
|
827
|
+
ended = true;
|
|
828
|
+
await invokeService({ kind: "end", handle });
|
|
829
|
+
};
|
|
830
|
+
try {
|
|
831
|
+
applyRequestEffects(event, answer);
|
|
832
|
+
const { internal } = await runtime();
|
|
833
|
+
if (answer.redirect) throw new internal.Redirect(answer.redirect.status, answer.redirect.location);
|
|
834
|
+
if (answer.error) throw new internal.HttpError(answer.error);
|
|
835
|
+
let final = answer.response;
|
|
836
|
+
if (answer.resolve) {
|
|
837
|
+
event.platform ??= {};
|
|
838
|
+
event.platform.skgoRequestHandle = handle;
|
|
839
|
+
const options = {};
|
|
840
|
+
if (answer.options?.transform) options.transformPageChunk = async ({ html, done }) => {
|
|
841
|
+
try {
|
|
842
|
+
const result = JSON.parse(await invokeService({ kind: "resolve-option", option: "transform", handle, html, done }));
|
|
843
|
+
if (typeof result.html !== "string") throw new Error("skgo prerender transform returned no HTML");
|
|
844
|
+
return result.html;
|
|
845
|
+
} catch (error) { failPredicateRelay(error); }
|
|
846
|
+
};
|
|
847
|
+
if (answer.options?.filter) options.filterSerializedResponseHeaders = (name, value) => predicate(handle, "filter", { name, value });
|
|
848
|
+
if (answer.options?.preload) options.preload = (input) => predicate(handle, "preload", { input });
|
|
849
|
+
const response = await resolveRequest(event, options);
|
|
850
|
+
const headers = {};
|
|
851
|
+
for (const [name, value] of response.headers) headers[name] = [value];
|
|
852
|
+
if (response.headers.getSetCookie) headers["set-cookie"] = response.headers.getSetCookie();
|
|
853
|
+
const result = JSON.parse(await invokeService({kind: "response", handle, response: {
|
|
854
|
+
status: response.status, headers,
|
|
855
|
+
body: Buffer.from(await response.arrayBuffer()).toString("base64"),
|
|
856
|
+
}}));
|
|
857
|
+
if (result.redirect) throw new internal.Redirect(result.redirect.status, result.redirect.location);
|
|
858
|
+
if (result.error) throw new internal.HttpError(result.error);
|
|
859
|
+
final = result.response;
|
|
860
|
+
}
|
|
861
|
+
if (!final || !Number.isInteger(final.status)) throw new Error("skgo prerender request omitted final response");
|
|
862
|
+
const headers = new Headers();
|
|
863
|
+
for (const [name, values] of Object.entries(final.headers ?? {}))
|
|
864
|
+
for (const value of values ?? []) headers.append(name, value);
|
|
865
|
+
if (event.request.method === "HEAD" || [204, 205, 304].includes(final.status)) {
|
|
866
|
+
await end();
|
|
867
|
+
return new Response(null, { status: final.status, headers });
|
|
868
|
+
}
|
|
869
|
+
const bytes = Buffer.from(final.body ?? "", "base64");
|
|
870
|
+
let emitted = false;
|
|
871
|
+
const body = new ReadableStream({
|
|
872
|
+
async pull(controller) {
|
|
873
|
+
try {
|
|
874
|
+
if (!emitted) { emitted = true; controller.enqueue(bytes); }
|
|
875
|
+
else { await end(); controller.close(); }
|
|
876
|
+
} catch (error) { controller.error(error); }
|
|
877
|
+
},
|
|
878
|
+
cancel: end,
|
|
879
|
+
}, { highWaterMark: 0 });
|
|
880
|
+
return new Response(body, { status: final.status, headers });
|
|
881
|
+
} catch (error) {
|
|
882
|
+
await end();
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** Kit has applied internal response cookies before its supplied fetch returns. */
|
|
888
|
+
export async function requestFetch({ event, request, fetch }) {
|
|
889
|
+
const scope = { internal: false };
|
|
890
|
+
return internalFetch.run(scope, async () => {
|
|
891
|
+
const response = await fetch(request);
|
|
892
|
+
const cookies = response.headers.getSetCookie();
|
|
893
|
+
const handle = event.platform?.skgoRequestHandle;
|
|
894
|
+
if (scope.internal && handle && cookies.length) {
|
|
895
|
+
// Match the order in which Kit applied concurrent fetch responses.
|
|
896
|
+
const previous = cookieRelays.get(event.platform) ?? Promise.resolve();
|
|
897
|
+
const relay = previous.then(() => invokeService({ kind: "cookies", handle, url: request.url, cookies }));
|
|
898
|
+
cookieRelays.set(event.platform, relay);
|
|
899
|
+
await relay;
|
|
900
|
+
}
|
|
901
|
+
return response;
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** A generated server-route stub forwards only its build-time execution. */
|
|
906
|
+
export async function remoteEndpoint(event) {
|
|
907
|
+
const answer = JSON.parse(await invokeService({kind: "endpoint", handle:event.platform?.skgoRequestHandle}));
|
|
908
|
+
applyRequestEffects(event,answer);
|
|
909
|
+
const response = answer.response;
|
|
910
|
+
if (!response || !Number.isInteger(response.status)) throw new Error("skgo prerender endpoint omitted response");
|
|
911
|
+
const headers = new Headers();
|
|
912
|
+
for (const [name,values] of Object.entries(response.headers ?? {}))
|
|
913
|
+
for (const value of values ?? []) headers.append(name,value);
|
|
914
|
+
const body = event.request.method === "HEAD" || [204,205,304].includes(response.status) ? null : Buffer.from(response.body ?? "","base64");
|
|
915
|
+
return new Response(body,{status:response.status,headers});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// The single Kit prerender worker executes predicates synchronously. Replies
|
|
919
|
+
// carry the result; the cell is only the bounded completion notification.
|
|
920
|
+
let predicateChannel;
|
|
921
|
+
let predicateFailure;
|
|
922
|
+
let predicateID = 0;
|
|
923
|
+
const predicateCell = new Int32Array(new SharedArrayBuffer(4));
|
|
924
|
+
function relayChannel() {
|
|
925
|
+
if (!predicateChannel) {
|
|
926
|
+
const name = process.env[PREDICATE_CHANNEL];
|
|
927
|
+
if (!name) throw new Error("skgo prerender predicate owner unavailable");
|
|
928
|
+
predicateChannel = new BroadcastChannel(name);
|
|
929
|
+
predicateChannel.unref();
|
|
930
|
+
}
|
|
931
|
+
return predicateChannel;
|
|
932
|
+
}
|
|
933
|
+
function failPredicateRelay(error) {
|
|
934
|
+
predicateFailure ??= error instanceof Error ? error : new Error(String(error));
|
|
935
|
+
try { relayChannel().postMessage({ type: "failure", error: predicateFailure.stack ?? predicateFailure.message }); } catch {}
|
|
936
|
+
throw predicateFailure;
|
|
937
|
+
}
|
|
938
|
+
function predicate(handle, option, args) {
|
|
939
|
+
if (predicateFailure) throw predicateFailure;
|
|
940
|
+
try {
|
|
941
|
+
const channel = relayChannel();
|
|
942
|
+
const id = ++predicateID;
|
|
943
|
+
Atomics.store(predicateCell, 0, 0);
|
|
944
|
+
channel.postMessage({ type: "predicate", id, cell: predicateCell, request: { handle, option, ...args } });
|
|
945
|
+
const status = Atomics.wait(predicateCell, 0, 0, CALLBACK_TIMEOUT);
|
|
946
|
+
if (status === "timed-out") throw new Error(`skgo prerender ${option} predicate timed out after ${CALLBACK_TIMEOUT}ms`);
|
|
947
|
+
const reply = receiveMessageOnPort(channel)?.message;
|
|
948
|
+
if (!reply || reply.id !== id || (typeof reply.value === "boolean") === (typeof reply.error === "string"))
|
|
949
|
+
throw new Error("skgo prerender predicate reply missing or malformed");
|
|
950
|
+
if (reply.error !== undefined) throw new Error(reply.error);
|
|
951
|
+
return reply.value;
|
|
952
|
+
} catch (error) { return failPredicateRelay(error); }
|
|
833
953
|
}
|