@skgo/sveltekit-adapter 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skgo-adapter/prerender.js +708 -785
- package/skgo-adapter.js +19 -3
|
@@ -1,824 +1,747 @@
|
|
|
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 } from "node:worker_threads";
|
|
10
11
|
|
|
11
|
-
const TAG = 'skgo-prerender-v1';
|
|
12
12
|
const owners = new Map();
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const SERVICE_URL = "SKGO_PRERENDER_SERVICE_URL";
|
|
14
|
+
const SERVICE_SECRET = "SKGO_PRERENDER_SERVICE_SECRET";
|
|
15
|
+
const CALLBACK_TIMEOUT = 30_000;
|
|
16
|
+
const STARTUP_TIMEOUT = 60_000;
|
|
17
|
+
const SHUTDOWN_GRACE = 150;
|
|
18
|
+
const CLEANUP_TIMEOUT = 5_000;
|
|
17
19
|
function appRoot() {
|
|
18
|
-
|
|
20
|
+
return resolve(process.cwd());
|
|
21
|
+
}
|
|
22
|
+
function reportOwnerFailure(error) {
|
|
23
|
+
const cleanup = error?.cleanupError ? `; cleanup failed: ${error.cleanupError.message}` : "";
|
|
24
|
+
process.stderr.write(`skgo prerender owner failure: ${error?.message ?? error}${cleanup}\n`);
|
|
19
25
|
}
|
|
20
|
-
|
|
21
26
|
function kitRootFor(root) {
|
|
22
|
-
|
|
27
|
+
return realpathSync(join(root, "node_modules/@sveltejs/kit"));
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
function kitQueueFailure(detail) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
const error = new Error(
|
|
32
|
+
`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".`,
|
|
33
|
+
);
|
|
34
|
+
error.name = "SKGO_KIT_PRERENDER_QUEUE";
|
|
35
|
+
error.code = "SKGO_KIT_PRERENDER_QUEUE";
|
|
36
|
+
return error;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
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
|
-
|
|
40
|
+
let metadata;
|
|
41
|
+
try {
|
|
42
|
+
const appRequire = createRequire(join(root, "package.json"));
|
|
43
|
+
const adapterPackage = appRequire.resolve("@skgo/sveltekit-adapter/package.json");
|
|
44
|
+
metadata = JSON.parse(
|
|
45
|
+
readFileSync(
|
|
46
|
+
join(dirname(adapterPackage), "skgo-adapter/compat/kit-3.0.0-queue.json"),
|
|
47
|
+
"utf8",
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw kitQueueFailure(`adapter compatibility metadata could not be read: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const kitRoot = kitRootFor(root);
|
|
55
|
+
const pkg = JSON.parse(readFileSync(join(kitRoot, "package.json"), "utf8"));
|
|
56
|
+
const queuePath = join(kitRoot, metadata.queuePath);
|
|
57
|
+
const queue = readFileSync(queuePath);
|
|
58
|
+
const digest = createHash("sha256").update(queue).digest("hex");
|
|
59
|
+
if (
|
|
60
|
+
pkg.name !== metadata.package ||
|
|
61
|
+
pkg.version !== metadata.version ||
|
|
62
|
+
digest !== metadata.correctedSHA256
|
|
63
|
+
) {
|
|
64
|
+
throw kitQueueFailure(
|
|
65
|
+
`resolved ${pkg.name ?? "unknown"}@${pkg.version ?? "unknown"} queue SHA-256 ${digest}; expected ${metadata.package}@${metadata.version} with SHA-256 ${metadata.correctedSHA256}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error?.code === "SKGO_KIT_PRERENDER_QUEUE") throw error;
|
|
70
|
+
throw kitQueueFailure(`resolved Kit queue could not be verified: ${error.message}`);
|
|
71
|
+
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
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 {}
|
|
75
|
+
if (
|
|
76
|
+
!isMainThread ||
|
|
77
|
+
process.env.NODE_PACKAGE_MANAGER !== "vite-plus" ||
|
|
78
|
+
process.argv[2] !== "build" ||
|
|
79
|
+
!process.argv[1]
|
|
80
|
+
)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
const appRequire = createRequire(join(root, "package.json"));
|
|
84
|
+
const vpRequire = createRequire(appRequire.resolve("vite-plus/package.json"));
|
|
85
|
+
const expected = realpathSync(join(dirname(vpRequire.resolve("vite")), "cli.js"));
|
|
86
|
+
return realpathSync(process.argv[1]) === expected && process.ppid > 1 ? process.ppid : null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
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
|
-
}, 50);
|
|
321
|
-
session.parentObserver.unref();
|
|
322
|
-
},
|
|
323
|
-
fail(error) {
|
|
324
|
-
if (!session.failed) session.failed = error;
|
|
325
|
-
session.closing = true;
|
|
326
|
-
return session.cleanup().catch((cleanupError) => {
|
|
327
|
-
session.cleanupFailure = cleanupError;
|
|
328
|
-
if (session.failed && typeof session.failed === 'object') session.failed.cleanupError = cleanupError;
|
|
329
|
-
throw session.failed ?? cleanupError;
|
|
330
|
-
});
|
|
331
|
-
},
|
|
332
|
-
async ensureCompiled() {
|
|
333
|
-
if (session.closing) throw session.failed ?? makeError('skgo prerender owner is closed');
|
|
334
|
-
if (!session.compilePromise) {
|
|
335
|
-
const config = generatedConfig(root);
|
|
336
|
-
session.cwd = config.cwd;
|
|
337
|
-
session.pkg = config.pkg;
|
|
338
|
-
session.dir = mkdtempSync(join(tmpdir(), 'skgo-prerender-'));
|
|
339
|
-
session.binary = join(session.dir, process.platform === 'win32' ? 'build.exe' : 'build');
|
|
340
|
-
const onExit = () => {
|
|
341
|
-
for (const job of session.groups) signalTree(job, 'SIGKILL');
|
|
342
|
-
try { rmSync(session.dir, { recursive: true, force: true }); } catch {}
|
|
343
|
-
};
|
|
344
|
-
process.once('exit', onExit);
|
|
345
|
-
session.exitHandler = onExit;
|
|
346
|
-
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
347
|
-
const handler = async () => {
|
|
348
|
-
try {
|
|
349
|
-
await session.fail(makeError(`skgo prerender build interrupted by ${signal}`));
|
|
350
|
-
process.off(signal, handler);
|
|
351
|
-
try { process.kill(process.pid, signal); } catch { process.exitCode = 1; }
|
|
352
|
-
} catch (error) {
|
|
353
|
-
process.exitCode = 1;
|
|
354
|
-
reportOwnerFailure(error);
|
|
355
|
-
process.once(signal, handler);
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
session.signalHandlers.set(signal, handler);
|
|
359
|
-
process.once(signal, handler);
|
|
360
|
-
}
|
|
361
|
-
mkdirSync(join(session.dir, 'go-tmp'), { recursive: true });
|
|
362
|
-
session.compilePromise = (async () => {
|
|
363
|
-
try {
|
|
364
|
-
const job = createJob(session, 'go', ['build', '-o', session.binary, session.pkg], {
|
|
365
|
-
cwd: session.cwd,
|
|
366
|
-
env: { ...process.env, GOTMPDIR: join(session.dir, 'go-tmp') }
|
|
367
|
-
});
|
|
368
|
-
const result = await job.done;
|
|
369
|
-
if (result.code !== 0) throw makeError(`skgo prerender compiler failed: ${result.stderr.trim()}`);
|
|
370
|
-
} catch (error) {
|
|
371
|
-
session.failed ??= error;
|
|
372
|
-
session.closing = true;
|
|
373
|
-
await session.cleanup();
|
|
374
|
-
throw error;
|
|
375
|
-
}
|
|
376
|
-
})();
|
|
377
|
-
}
|
|
378
|
-
await session.compilePromise;
|
|
379
|
-
if (session.closing) throw session.failed ?? makeError('skgo prerender owner is closed');
|
|
380
|
-
},
|
|
381
|
-
async invoke(request) {
|
|
382
|
-
await session.ensureCompiled();
|
|
383
|
-
if (session.closing) throw session.failed ?? makeError('skgo prerender owner is closed');
|
|
384
|
-
const job = createJob(session, session.binary, [], {
|
|
385
|
-
cwd: session.cwd,
|
|
386
|
-
input: JSON.stringify(request),
|
|
387
|
-
request
|
|
388
|
-
});
|
|
389
|
-
try {
|
|
390
|
-
const result = await job.done;
|
|
391
|
-
if (session.closing) throw session.failed ?? makeError('skgo prerender owner is closed');
|
|
392
|
-
return result.stdout;
|
|
393
|
-
} catch (error) {
|
|
394
|
-
await session.fail(error);
|
|
395
|
-
throw error;
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
async cleanup() {
|
|
399
|
-
if (session.cleanupPromise) return session.cleanupPromise;
|
|
400
|
-
session.closing = true;
|
|
401
|
-
const cleanupPromise = (async () => {
|
|
402
|
-
const jobs = [...session.jobs];
|
|
403
|
-
const escalations = jobs.map(startDrain);
|
|
404
|
-
// Start TERM-to-KILL escalation before waiting for children to close.
|
|
405
|
-
await Promise.allSettled([...jobs.map((job) => job.closePromise), ...escalations]);
|
|
406
|
-
const drainErrors = [];
|
|
407
|
-
for (const job of [...session.groups]) {
|
|
408
|
-
try {
|
|
409
|
-
await stopGroup(job);
|
|
410
|
-
session.groups.delete(job);
|
|
411
|
-
} catch (error) {
|
|
412
|
-
drainErrors.push(error);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (drainErrors.length) {
|
|
416
|
-
const error = new AggregateError(drainErrors, 'skgo could not drain every owned prerender process tree');
|
|
417
|
-
session.cleanupFailure = error;
|
|
418
|
-
if (session.failed && typeof session.failed === 'object') session.failed.cleanupError = error;
|
|
419
|
-
throw error;
|
|
420
|
-
}
|
|
421
|
-
for (const workerRecord of session.workers.values()) {
|
|
422
|
-
for (const requestId of workerRecord.handshakes) {
|
|
423
|
-
try { workerRecord.worker.postMessage({ tag: TAG, type: 'failed', appRoot: root, session: session.id, requestId, error: session.failed?.message ?? 'owner closed' }); } catch {}
|
|
424
|
-
}
|
|
425
|
-
workerRecord.handshakes.clear();
|
|
426
|
-
for (const requestId of workerRecord.pending.keys()) {
|
|
427
|
-
try { workerRecord.worker.postMessage({ tag: TAG, type: 'failed', appRoot: root, session: session.id, requestId, error: session.failed?.message ?? 'owner closed' }); } catch {}
|
|
428
|
-
}
|
|
429
|
-
workerRecord.pending.clear();
|
|
430
|
-
workerRecord.worker.off('message', workerRecord.onMessage);
|
|
431
|
-
workerRecord.worker.off('error', workerRecord.onError);
|
|
432
|
-
workerRecord.worker.off('exit', workerRecord.onExit);
|
|
433
|
-
}
|
|
434
|
-
for (const [worker, listener] of session.observedWorkers) worker.off('message', listener);
|
|
435
|
-
session.observedWorkers.clear();
|
|
436
|
-
session.workers.clear();
|
|
437
|
-
if (session.dir) rmSync(session.dir, { recursive: true, force: true });
|
|
438
|
-
if (session.exitHandler) process.off('exit', session.exitHandler);
|
|
439
|
-
if (session.parentObserver) clearInterval(session.parentObserver);
|
|
440
|
-
session.parentObserver = null;
|
|
441
|
-
for (const [signal, handler] of session.signalHandlers) process.off(signal, handler);
|
|
442
|
-
process.off('worker', session.onWorker);
|
|
443
|
-
session.jobs.clear();
|
|
444
|
-
session.groups.clear();
|
|
445
|
-
ownerByRoot.delete(root);
|
|
446
|
-
})();
|
|
447
|
-
session.cleanupPromise = cleanupPromise;
|
|
448
|
-
try {
|
|
449
|
-
await cleanupPromise;
|
|
450
|
-
} catch (error) {
|
|
451
|
-
// Retain listeners, directories and failed group records so later
|
|
452
|
-
// cleanup attempts can retry rather than forgetting live ownership.
|
|
453
|
-
session.cleanupPromise = null;
|
|
454
|
-
throw error;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
return session;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function response(worker, message, fields) {
|
|
462
|
-
try { worker.postMessage({ tag: TAG, appRoot: message.appRoot, session: message.session, requestId: message.requestId, ...fields }); } catch {}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async function connectWorker(session, worker, message) {
|
|
466
|
-
if (session.closing) {
|
|
467
|
-
await session.cleanup();
|
|
468
|
-
response(worker, message, { type: 'failed', error: session.failed?.message ?? 'owner is closed' });
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
let record = session.workers.get(worker);
|
|
472
|
-
if (!record) {
|
|
473
|
-
record = { worker, pending: new Map(), handshakes: new Set(), exited: false };
|
|
474
|
-
session.workers.set(worker, record);
|
|
475
|
-
record.onMessage = (next) => {
|
|
476
|
-
if (!next || next.tag !== TAG || next.appRoot !== session.root || next.session !== session.id) return;
|
|
477
|
-
if (next.type === 'connected-ack') {
|
|
478
|
-
record.handshakes.delete(next.requestId);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
void handleWorkerRequest(session, record, next).catch(reportOwnerFailure);
|
|
482
|
-
};
|
|
483
|
-
record.onError = (error) => { void session.fail(error).catch(reportOwnerFailure); };
|
|
484
|
-
record.onExit = (code) => {
|
|
485
|
-
record.exited = true;
|
|
486
|
-
if (record.pending.size > 0 || record.handshakes.size > 0 || code !== 0) {
|
|
487
|
-
session.closing = true;
|
|
488
|
-
void session.fail(makeError(`Kit prerender worker exited ${code} with ${record.pending.size} pending Go requests and ${record.handshakes.size} pending handshakes`)).catch(reportOwnerFailure);
|
|
489
|
-
}
|
|
490
|
-
};
|
|
491
|
-
worker.on('message', record.onMessage);
|
|
492
|
-
worker.on('error', record.onError);
|
|
493
|
-
worker.on('exit', record.onExit);
|
|
494
|
-
}
|
|
495
|
-
record.handshakes.add(message.requestId);
|
|
496
|
-
try { worker.postMessage({ tag: TAG, type: 'connected', appRoot: session.root, session: session.id, requestId: message.requestId }); }
|
|
497
|
-
catch (error) {
|
|
498
|
-
record.handshakes.delete(message.requestId);
|
|
499
|
-
await session.fail(error);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async function handleWorkerRequest(session, record, message) {
|
|
504
|
-
if (record.exited) return;
|
|
505
|
-
if (session.closing) {
|
|
506
|
-
await session.cleanup();
|
|
507
|
-
response(record.worker, message, { type: 'failed', error: session.failed?.message ?? 'owner is closed' });
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
if (message.type === 'fatal') {
|
|
511
|
-
record.pending.set(message.requestId, { worker: record.worker, requestId: message.requestId });
|
|
512
|
-
await session.fail(makeError(`Kit prerender worker reported a fatal protocol error: ${message.error ?? 'unknown error'}`));
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
if (message.type !== 'invoke') {
|
|
516
|
-
response(record.worker, message, { type: 'failed', error: session.failed?.message ?? 'owner is closed' });
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
const pending = { worker: record.worker, requestId: message.requestId };
|
|
520
|
-
record.pending.set(message.requestId, pending);
|
|
521
|
-
try {
|
|
522
|
-
const raw = await session.invoke(message.request);
|
|
523
|
-
if (record.exited || session.closing) throw session.failed ?? makeError('owner closed while request was running');
|
|
524
|
-
record.pending.delete(message.requestId);
|
|
525
|
-
response(record.worker, message, { type: 'result', raw });
|
|
526
|
-
} catch (error) {
|
|
527
|
-
await session.fail(error);
|
|
528
|
-
record.pending.delete(message.requestId);
|
|
529
|
-
response(record.worker, message, { type: 'failed', error: error.message });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/** Create the process owner from the main Vite build thread. */
|
|
93
|
+
let value;
|
|
94
|
+
try {
|
|
95
|
+
value = JSON.parse(raw);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new Error(`skgo prerender command returned malformed JSON: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
100
|
+
throw new Error("skgo prerender command returned a non-object response");
|
|
101
|
+
if (request.kind === "remote-inputs") {
|
|
102
|
+
if (typeof value.inputs !== "string")
|
|
103
|
+
throw new Error("skgo prerender inputs response has no devalue payload");
|
|
104
|
+
} else if (request.kind === "remote") {
|
|
105
|
+
if (value.type === "result" && typeof value.data === "string") return;
|
|
106
|
+
if (
|
|
107
|
+
value.type === "error" &&
|
|
108
|
+
value.kind === "app" &&
|
|
109
|
+
value.error &&
|
|
110
|
+
Number.isInteger(value.error.status) &&
|
|
111
|
+
typeof value.error.message === "string"
|
|
112
|
+
)
|
|
113
|
+
return;
|
|
114
|
+
if (
|
|
115
|
+
value.type === "error" &&
|
|
116
|
+
value.kind === "unknown" &&
|
|
117
|
+
value.error &&
|
|
118
|
+
Number.isInteger(value.error.status) &&
|
|
119
|
+
typeof value.error.message === "string" &&
|
|
120
|
+
typeof value.diagnostic === "string"
|
|
121
|
+
)
|
|
122
|
+
return;
|
|
123
|
+
if (
|
|
124
|
+
value.type === "redirect" &&
|
|
125
|
+
Number.isInteger(value.redirect?.status) &&
|
|
126
|
+
value.redirect.status >= 300 &&
|
|
127
|
+
value.redirect.status <= 308 &&
|
|
128
|
+
typeof value.redirect.location === "string"
|
|
129
|
+
)
|
|
130
|
+
return;
|
|
131
|
+
throw new Error("skgo prerender remote response has an invalid envelope");
|
|
132
|
+
} else if (request.kind === "load") {
|
|
133
|
+
const allowed = ["data", "chunks", "error", "failure", "redirect", "headers", "cookies"];
|
|
134
|
+
if (!allowed.some((key) => key in value))
|
|
135
|
+
throw new Error("skgo prerender load response has no recognized fields");
|
|
136
|
+
if (
|
|
137
|
+
value.data !== undefined &&
|
|
138
|
+
typeof value.data !== "string" &&
|
|
139
|
+
!(value.data && typeof value.data === "object")
|
|
140
|
+
)
|
|
141
|
+
throw new Error("skgo prerender load response has invalid data");
|
|
142
|
+
if (value.failure !== undefined && typeof value.failure !== "string")
|
|
143
|
+
throw new Error("skgo prerender load response has invalid failure");
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error(`skgo prerender owner received unsupported request kind ${request.kind}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function alive(job) {
|
|
150
|
+
if (!job.child.pid) return false;
|
|
151
|
+
try {
|
|
152
|
+
process.kill(process.platform === "win32" ? job.child.pid : -job.child.pid, 0);
|
|
153
|
+
return true;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error.code === "ESRCH") return false;
|
|
156
|
+
// EPERM is an existence result for signal 0; actual termination below still
|
|
157
|
+
// reports permission failures rather than mistaking this probe for a drain.
|
|
158
|
+
if (error.code === "EPERM") return true;
|
|
159
|
+
error.message = `skgo process group ${job.child.pid} probe failed: ${error.message}`;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function signalTree(job, force) {
|
|
164
|
+
if (!job.child.pid) return;
|
|
165
|
+
if (process.platform === "win32") {
|
|
166
|
+
await bounded(
|
|
167
|
+
new Promise((resolve, reject) => {
|
|
168
|
+
const args = ["/PID", String(job.child.pid), "/T", ...(force ? ["/F"] : [])];
|
|
169
|
+
const killer = spawn("taskkill", args, { stdio: "ignore", windowsHide: true });
|
|
170
|
+
killer.once("error", reject);
|
|
171
|
+
killer.once("close", resolve);
|
|
172
|
+
}),
|
|
173
|
+
CLEANUP_TIMEOUT,
|
|
174
|
+
"skgo prerender taskkill did not finish",
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(-job.child.pid, force ? "SIGKILL" : "SIGTERM");
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.code === "ESRCH") return;
|
|
181
|
+
// Darwin can reject signals before Node observes an exited child.
|
|
182
|
+
// Retain the error; only bounded reaping and group absence prove cleanup.
|
|
183
|
+
if (error.code === "EPERM") job.signalError ??= error;
|
|
184
|
+
else throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function delay(ms) {
|
|
189
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
190
|
+
}
|
|
191
|
+
async function bounded(promise, ms, message) {
|
|
192
|
+
let timer;
|
|
193
|
+
try {
|
|
194
|
+
return await Promise.race([
|
|
195
|
+
promise,
|
|
196
|
+
new Promise((_, reject) => {
|
|
197
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
198
|
+
}),
|
|
199
|
+
]);
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function stopJob(job, cooperative = false) {
|
|
205
|
+
if (cooperative) {
|
|
206
|
+
job.control?.end();
|
|
207
|
+
}
|
|
208
|
+
if (cooperative || job.hasExited) {
|
|
209
|
+
// Join the child's close before probing its just-exited process group.
|
|
210
|
+
// Descendants holding inherited output pipes still hit the same fixed grace.
|
|
211
|
+
await bounded(job.closed, SHUTDOWN_GRACE, "shutdown grace elapsed").catch((error) => {
|
|
212
|
+
if (error.message !== "shutdown grace elapsed") throw error;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (alive(job)) {
|
|
216
|
+
await signalTree(job, false);
|
|
217
|
+
await delay(150);
|
|
218
|
+
if (alive(job)) await signalTree(job, true);
|
|
219
|
+
}
|
|
220
|
+
await bounded(
|
|
221
|
+
job.closed,
|
|
222
|
+
CLEANUP_TIMEOUT,
|
|
223
|
+
`skgo prerender owned process did not close${job.signalError ? ": " + job.signalError.message : ""}`,
|
|
224
|
+
);
|
|
225
|
+
const deadline = Date.now() + CLEANUP_TIMEOUT;
|
|
226
|
+
while (alive(job) && Date.now() < deadline) await delay(20);
|
|
227
|
+
if (alive(job))
|
|
228
|
+
throw new Error(`skgo could not drain process group ${job.child.pid}`, {
|
|
229
|
+
cause: job.signalError,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// Node's generated stdin pipe is write-only at the parent. On Windows, pass
|
|
233
|
+
// a connected duplex pipe as stdin so Go receives its native inherited handle.
|
|
234
|
+
async function windowsControl() {
|
|
235
|
+
const listener = createServer();
|
|
236
|
+
const path = `\\\\.\\pipe\\skgo-prerender-${randomUUID()}`;
|
|
237
|
+
let childEnd, parentEnd;
|
|
238
|
+
const accepted = new Promise((resolve, reject) => {
|
|
239
|
+
listener.once("connection", (socket) => {
|
|
240
|
+
parentEnd = socket;
|
|
241
|
+
resolve(socket);
|
|
242
|
+
});
|
|
243
|
+
listener.once("error", reject);
|
|
244
|
+
});
|
|
245
|
+
accepted.catch(() => {});
|
|
246
|
+
try {
|
|
247
|
+
await bounded(
|
|
248
|
+
new Promise((resolve, reject) => {
|
|
249
|
+
listener.once("error", reject);
|
|
250
|
+
listener.listen(path, resolve);
|
|
251
|
+
}),
|
|
252
|
+
STARTUP_TIMEOUT,
|
|
253
|
+
"skgo prerender control pipe startup timed out",
|
|
254
|
+
);
|
|
255
|
+
childEnd = createConnection(path);
|
|
256
|
+
const connected = new Promise((resolve, reject) => {
|
|
257
|
+
childEnd.once("connect", resolve);
|
|
258
|
+
childEnd.once("error", reject);
|
|
259
|
+
});
|
|
260
|
+
await bounded(
|
|
261
|
+
Promise.all([accepted, connected]),
|
|
262
|
+
STARTUP_TIMEOUT,
|
|
263
|
+
"skgo prerender control pipe startup timed out",
|
|
264
|
+
);
|
|
265
|
+
listener.close();
|
|
266
|
+
return { childEnd, parentEnd };
|
|
267
|
+
} catch (error) {
|
|
268
|
+
childEnd?.destroy();
|
|
269
|
+
parentEnd?.destroy();
|
|
270
|
+
listener.close();
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function startJob(owner, command, args, options = {}) {
|
|
276
|
+
const child = spawn(command, args, {
|
|
277
|
+
cwd: owner.cwd,
|
|
278
|
+
env: options.env ?? process.env,
|
|
279
|
+
stdio: options.service
|
|
280
|
+
? options.control
|
|
281
|
+
? [options.control.childEnd, "pipe", "pipe"]
|
|
282
|
+
: ["ignore", "pipe", "pipe", "pipe"]
|
|
283
|
+
: ["ignore", "pipe", "pipe"],
|
|
284
|
+
detached: process.platform !== "win32",
|
|
285
|
+
windowsHide: true,
|
|
286
|
+
});
|
|
287
|
+
const job = {
|
|
288
|
+
child,
|
|
289
|
+
stderr: "",
|
|
290
|
+
control: options.service ? (options.control?.parentEnd ?? child.stdio[3]) : null,
|
|
291
|
+
};
|
|
292
|
+
options.control?.childEnd.destroy();
|
|
293
|
+
owner.jobs.add(job);
|
|
294
|
+
job.exited = new Promise((resolve, reject) => {
|
|
295
|
+
child.once("error", reject);
|
|
296
|
+
child.once("exit", (code, signal) => {
|
|
297
|
+
job.hasExited = true;
|
|
298
|
+
resolve({ code, signal });
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
job.closed = new Promise((resolve) => {
|
|
302
|
+
child.once("close", resolve);
|
|
303
|
+
});
|
|
304
|
+
job.exited.catch(() => {});
|
|
305
|
+
child.stdout.on("data", (chunk) => {
|
|
306
|
+
if (options.service) process.stdout.write(chunk);
|
|
307
|
+
});
|
|
308
|
+
child.stderr.on("data", (chunk) => {
|
|
309
|
+
if (options.service) process.stderr.write(chunk);
|
|
310
|
+
else job.stderr += chunk;
|
|
311
|
+
});
|
|
312
|
+
job.control?.on("error", () => {});
|
|
313
|
+
// Descendants can inherit stdout/stderr and hold close open after the leader exits.
|
|
314
|
+
child.once("exit", () => {
|
|
315
|
+
void signalTree(job, false).catch(reportOwnerFailure);
|
|
316
|
+
});
|
|
317
|
+
return job;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The main build thread owns its compiler, HTTP service and scratch directory. */
|
|
534
321
|
export function createPrerenderOwner(root = appRoot()) {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
322
|
+
if (!isMainThread) return null;
|
|
323
|
+
root = resolve(root);
|
|
324
|
+
if (owners.has(root)) return owners.get(root);
|
|
325
|
+
const manifest = JSON.parse(readFileSync(join(root, "skgo.remotes.json"), "utf8"));
|
|
326
|
+
// The generator emits this entry only when loads or prerender remotes exist.
|
|
327
|
+
if (!manifest.prerender) return null;
|
|
328
|
+
const owner = {
|
|
329
|
+
root,
|
|
330
|
+
cwd: resolve(root, manifest.prerender.root),
|
|
331
|
+
pkg: manifest.prerender.package,
|
|
332
|
+
jobs: new Set(),
|
|
333
|
+
closing: false,
|
|
334
|
+
failed: null,
|
|
335
|
+
dir: null,
|
|
336
|
+
service: null,
|
|
337
|
+
previousEnv: null,
|
|
338
|
+
signals: new Map(),
|
|
339
|
+
cleanupPromise: null,
|
|
340
|
+
preparePromise: null,
|
|
341
|
+
fail(error) {
|
|
342
|
+
owner.failed ??= error;
|
|
343
|
+
return owner.cleanup().catch((cleanupError) => {
|
|
344
|
+
owner.cleanupFailure = cleanupError;
|
|
345
|
+
if (typeof owner.failed === "object" && owner.failed !== cleanupError) {
|
|
346
|
+
if (!owner.failed.cleanupError) {
|
|
347
|
+
const detail =
|
|
348
|
+
cleanupError.errors?.map((error) => error.message).join("; ") ?? cleanupError.message;
|
|
349
|
+
const diagnostic = `cleanup failed: ${detail}`;
|
|
350
|
+
owner.failed.message += `; ${diagnostic}`;
|
|
351
|
+
if (owner.failed.stack) owner.failed.stack += `\n${diagnostic}`;
|
|
352
|
+
}
|
|
353
|
+
owner.failed.cleanupError = cleanupError;
|
|
354
|
+
}
|
|
355
|
+
throw owner.failed;
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
prepare() {
|
|
359
|
+
return (owner.preparePromise ??= (async () => {
|
|
360
|
+
owner.dir = mkdtempSync(join(tmpdir(), "skgo-prerender-"));
|
|
361
|
+
mkdirSync(join(owner.dir, "go-tmp"));
|
|
362
|
+
const binary = join(owner.dir, process.platform === "win32" ? "build.exe" : "build");
|
|
363
|
+
try {
|
|
364
|
+
const compiler = startJob(owner, "go", ["build", "-o", binary, owner.pkg], {
|
|
365
|
+
env: { ...process.env, GOTMPDIR: join(owner.dir, "go-tmp") },
|
|
366
|
+
});
|
|
367
|
+
const result = await compiler.exited;
|
|
368
|
+
await stopJob(compiler);
|
|
369
|
+
owner.jobs.delete(compiler);
|
|
370
|
+
if (result.code !== 0)
|
|
371
|
+
throw new Error(`skgo prerender compiler failed: ${compiler.stderr.trim()}`);
|
|
372
|
+
if (owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
373
|
+
const control = process.platform === "win32" ? await windowsControl() : null;
|
|
374
|
+
if (owner.closing) {
|
|
375
|
+
control?.childEnd.destroy();
|
|
376
|
+
control?.parentEnd.destroy();
|
|
377
|
+
throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
378
|
+
}
|
|
379
|
+
let service;
|
|
380
|
+
try {
|
|
381
|
+
service = owner.service = startJob(owner, binary, [], { service: true, control });
|
|
382
|
+
} catch (error) {
|
|
383
|
+
control?.childEnd.destroy();
|
|
384
|
+
control?.parentEnd.destroy();
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
const ready = new Promise((resolve, reject) => {
|
|
388
|
+
let data = "";
|
|
389
|
+
const receive = (chunk) => {
|
|
390
|
+
data += chunk;
|
|
391
|
+
if (!data.includes("\n")) return;
|
|
392
|
+
service.control.off("data", receive);
|
|
393
|
+
try {
|
|
394
|
+
const value = JSON.parse(data.slice(0, data.indexOf("\n")));
|
|
395
|
+
const url = new URL(value.url);
|
|
396
|
+
if (
|
|
397
|
+
url.protocol !== "http:" ||
|
|
398
|
+
url.hostname !== "127.0.0.1" ||
|
|
399
|
+
!url.port ||
|
|
400
|
+
typeof value.secret !== "string" ||
|
|
401
|
+
!value.secret
|
|
402
|
+
)
|
|
403
|
+
throw new Error("invalid readiness");
|
|
404
|
+
resolve(value);
|
|
405
|
+
} catch {
|
|
406
|
+
reject(new Error("skgo prerender service returned invalid readiness"));
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
service.control.setEncoding("utf8");
|
|
410
|
+
service.control.on("data", receive);
|
|
411
|
+
service.control.once("error", () =>
|
|
412
|
+
reject(new Error("skgo prerender service control channel failed")),
|
|
413
|
+
);
|
|
414
|
+
service.exited.then(
|
|
415
|
+
({ code, signal }) =>
|
|
416
|
+
reject(
|
|
417
|
+
new Error(`skgo prerender service exited before readiness: ${code ?? signal}`),
|
|
418
|
+
),
|
|
419
|
+
reject,
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
owner.connection = await bounded(
|
|
423
|
+
ready,
|
|
424
|
+
STARTUP_TIMEOUT,
|
|
425
|
+
"skgo prerender service startup timed out",
|
|
426
|
+
);
|
|
427
|
+
process.stderr.write(`skgo prerender service started (pid ${service.child.pid})\n`);
|
|
428
|
+
service.exited.then(
|
|
429
|
+
({ code, signal }) => {
|
|
430
|
+
if (!owner.closing || code !== 0) {
|
|
431
|
+
const error = new Error(
|
|
432
|
+
`skgo prerender service exited unexpectedly: ${code ?? signal}`,
|
|
433
|
+
);
|
|
434
|
+
owner.failed ??= error;
|
|
435
|
+
if (!owner.closing) void owner.fail(error).catch(reportOwnerFailure);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
(error) => {
|
|
439
|
+
if (!owner.closing) void owner.fail(error).catch(reportOwnerFailure);
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
await owner.fail(error);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
})());
|
|
447
|
+
},
|
|
448
|
+
publish() {
|
|
449
|
+
if (owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
|
|
450
|
+
if (owner.previousEnv) return;
|
|
451
|
+
owner.previousEnv = new Map([
|
|
452
|
+
[SERVICE_URL, process.env[SERVICE_URL]],
|
|
453
|
+
[SERVICE_SECRET, process.env[SERVICE_SECRET]],
|
|
454
|
+
]);
|
|
455
|
+
process.env[SERVICE_URL] = owner.connection.url;
|
|
456
|
+
process.env[SERVICE_SECRET] = owner.connection.secret;
|
|
457
|
+
},
|
|
458
|
+
cleanup() {
|
|
459
|
+
if (owner.cleanupPromise) return owner.cleanupPromise;
|
|
460
|
+
owner.closing = true;
|
|
461
|
+
owner.cleanupPromise = (async () => {
|
|
462
|
+
const errors = [];
|
|
463
|
+
for (const job of owner.jobs) {
|
|
464
|
+
try {
|
|
465
|
+
await stopJob(job, job === owner.service);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
errors.push(error);
|
|
468
|
+
} finally {
|
|
469
|
+
job.control?.destroy();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (owner.previousEnv)
|
|
473
|
+
for (const [key, value] of owner.previousEnv) {
|
|
474
|
+
if (value === undefined) delete process.env[key];
|
|
475
|
+
else process.env[key] = value;
|
|
476
|
+
}
|
|
477
|
+
for (const [signal, handler] of owner.signals) process.off(signal, handler);
|
|
478
|
+
if (owner.parentObserver) clearInterval(owner.parentObserver);
|
|
479
|
+
process.off("exit", owner.exitHandler);
|
|
480
|
+
if (!errors.length && owner.dir) rmSync(owner.dir, { recursive: true, force: true });
|
|
481
|
+
owners.delete(root);
|
|
482
|
+
if (owner.service && !errors.length)
|
|
483
|
+
process.stderr.write(`skgo prerender service stopped (pid ${owner.service.child.pid})\n`);
|
|
484
|
+
if (errors.length)
|
|
485
|
+
throw new AggregateError(errors, "skgo could not drain owned prerender processes");
|
|
486
|
+
})();
|
|
487
|
+
return owner.cleanupPromise;
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
owner.exitHandler = () => {
|
|
491
|
+
for (const job of owner.jobs) {
|
|
492
|
+
if (process.platform !== "win32") {
|
|
493
|
+
try {
|
|
494
|
+
process.kill(-job.child.pid, "SIGKILL");
|
|
495
|
+
} catch {}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
process.once("exit", owner.exitHandler);
|
|
500
|
+
const interrupted = async (error, signal) => {
|
|
501
|
+
try {
|
|
502
|
+
await owner.fail(error);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
reportOwnerFailure(error);
|
|
505
|
+
}
|
|
506
|
+
process.exitCode = 1;
|
|
507
|
+
try {
|
|
508
|
+
process.kill(process.pid, signal);
|
|
509
|
+
} catch {
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
514
|
+
const handler = () => {
|
|
515
|
+
void interrupted(new Error(`skgo prerender build interrupted by ${signal}`), signal);
|
|
516
|
+
};
|
|
517
|
+
owner.signals.set(signal, handler);
|
|
518
|
+
process.on(signal, handler);
|
|
519
|
+
}
|
|
520
|
+
const parent = vitePlusBuildParent(root);
|
|
521
|
+
if (parent !== null) {
|
|
522
|
+
owner.parentObserver = setInterval(() => {
|
|
523
|
+
if (process.ppid === parent) return;
|
|
524
|
+
clearInterval(owner.parentObserver);
|
|
525
|
+
void interrupted(new Error("skgo prerender Vite build parent exited"), "SIGTERM");
|
|
526
|
+
}, 50);
|
|
527
|
+
owner.parentObserver.unref();
|
|
528
|
+
}
|
|
529
|
+
owners.set(root, owner);
|
|
530
|
+
return owner;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Vite 8's native buildApp has no failure-finally and its CLI exits immediately.
|
|
534
|
+
* Wrap only those whole-app hooks so their rejection waits for the owner. */
|
|
556
535
|
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;
|
|
536
|
+
if (!owner || typeof config.getSortedPlugins !== "function") return;
|
|
537
|
+
const wrapped = (owner.wrapped ??= new WeakSet());
|
|
538
|
+
const wrap = (handler) => {
|
|
539
|
+
if (wrapped.has(handler)) return handler;
|
|
540
|
+
const replacement = async function (...args) {
|
|
541
|
+
try {
|
|
542
|
+
const result = await handler.apply(this, args);
|
|
543
|
+
if (owner.failed) throw owner.failed;
|
|
544
|
+
return result;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
await owner.fail(error);
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
wrapped.add(replacement);
|
|
551
|
+
return replacement;
|
|
552
|
+
};
|
|
553
|
+
for (const plugin of config.getSortedPlugins("buildApp")) {
|
|
554
|
+
const hook = plugin.buildApp;
|
|
555
|
+
if (typeof hook === "function") plugin.buildApp = wrap(hook);
|
|
556
|
+
else if (hook?.handler) plugin.buildApp = { ...hook, handler: wrap(hook.handler) };
|
|
557
|
+
}
|
|
558
|
+
if (typeof config.builder?.buildApp === "function")
|
|
559
|
+
config.builder.buildApp = wrap(config.builder.buildApp);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function invokeService(request) {
|
|
563
|
+
const url = process.env[SERVICE_URL];
|
|
564
|
+
const secret = process.env[SERVICE_SECRET];
|
|
565
|
+
const identity = `${request.kind} ${request.module}/${request.name ?? ""}`;
|
|
566
|
+
if (!url || !secret) throw new Error(`skgo prerender service unavailable for ${identity}`);
|
|
567
|
+
const { kind, ...body } = request;
|
|
568
|
+
const path = { load: "/load", remote: "/remote", "remote-inputs": "/inputs" }[kind];
|
|
569
|
+
const controller = new AbortController();
|
|
570
|
+
const timer = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT);
|
|
571
|
+
try {
|
|
572
|
+
const response = await fetch(url + path, {
|
|
573
|
+
method: "POST",
|
|
574
|
+
headers: { authorization: `Bearer ${secret}`, "content-type": "application/json" },
|
|
575
|
+
body: JSON.stringify(body),
|
|
576
|
+
signal: controller.signal,
|
|
577
|
+
});
|
|
578
|
+
const raw = await response.text();
|
|
579
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
580
|
+
validateEnvelope(request, raw);
|
|
581
|
+
return raw;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (controller.signal.aborted) {
|
|
584
|
+
const message = `skgo prerender ${identity} timed out after ${CALLBACK_TIMEOUT}ms`;
|
|
585
|
+
process.stderr.write(`${message}\n`);
|
|
586
|
+
throw new Error(message);
|
|
587
|
+
}
|
|
588
|
+
throw new Error(`skgo prerender ${identity} failed: ${error.message}`);
|
|
589
|
+
} finally {
|
|
590
|
+
clearTimeout(timer);
|
|
591
|
+
}
|
|
702
592
|
}
|
|
703
593
|
|
|
704
594
|
async function kitRuntime(root) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
595
|
+
const kitRoot = kitRootFor(root);
|
|
596
|
+
const transportURL = pathToFileURL(join(kitRoot, "src/runtime/app/internal/transport.js")).href;
|
|
597
|
+
const internalURL = pathToFileURL(join(kitRoot, "src/exports/internal/shared.js")).href;
|
|
598
|
+
const transport = await import(transportURL);
|
|
599
|
+
const hooks = join(root, ".svelte-kit/output/server/entries/hooks.universal.js");
|
|
600
|
+
let definitions = {};
|
|
601
|
+
try {
|
|
602
|
+
({ transport: definitions = {} } = await import(pathToFileURL(hooks).href));
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
605
|
+
}
|
|
606
|
+
transport.init_transport(definitions);
|
|
607
|
+
const internal = await import(internalURL);
|
|
608
|
+
const require = createRequire(join(kitRoot, "package.json"));
|
|
609
|
+
const devalue = await import(pathToFileURL(require.resolve("devalue")).href);
|
|
610
|
+
return { transport, internal, devalue };
|
|
720
611
|
}
|
|
721
612
|
|
|
722
613
|
let runtimePromise;
|
|
723
614
|
function runtime() {
|
|
724
|
-
|
|
725
|
-
|
|
615
|
+
if (!runtimePromise) runtimePromise = kitRuntime(appRoot());
|
|
616
|
+
return runtimePromise;
|
|
726
617
|
}
|
|
727
618
|
|
|
728
619
|
/** Called by the generated native Kit input producer. */
|
|
729
620
|
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
|
-
}
|
|
621
|
+
verifyKitQueue(appRoot());
|
|
622
|
+
const raw = await invokeService({ kind: "remote-inputs", module, name });
|
|
623
|
+
const answer = JSON.parse(raw);
|
|
624
|
+
const { transport } = await runtime();
|
|
625
|
+
const values = transport.parse(answer.inputs);
|
|
626
|
+
if (!Array.isArray(values))
|
|
627
|
+
throw new Error("skgo prerender inputs response did not decode to an array");
|
|
628
|
+
return values;
|
|
743
629
|
}
|
|
744
630
|
|
|
745
631
|
/** The emulator callback for a Go load, executing entirely in its Kit worker. */
|
|
746
632
|
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
|
-
|
|
633
|
+
const kit = await runtime();
|
|
634
|
+
const { devalue, transport } = kit;
|
|
635
|
+
const headers = Object.fromEntries(
|
|
636
|
+
[...event.request.headers].map(([name, value]) => [name, [value]]),
|
|
637
|
+
);
|
|
638
|
+
const cookies = event.cookies.getAll();
|
|
639
|
+
if (cookies.length) {
|
|
640
|
+
headers.cookie = [
|
|
641
|
+
cookies
|
|
642
|
+
.map(({ name, value }) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
|
643
|
+
.join("; "),
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
// Kit's JS matcher results cannot construct named Go values. Send its
|
|
647
|
+
// actual route metadata and the unconverted path to the build-only Go load.
|
|
648
|
+
const { manifest } = await import(
|
|
649
|
+
pathToFileURL(join(appRoot(), ".svelte-kit/output/server/manifest-full.js")).href
|
|
650
|
+
);
|
|
651
|
+
const route = manifest.routes.find((candidate) => candidate.id === event.route.id);
|
|
652
|
+
const prefix = manifest.app_path.slice(0, -manifest.app_dir.length);
|
|
653
|
+
const base = prefix ? "/" + prefix.slice(0, -1) : "";
|
|
654
|
+
const { decode_pathname } = await import(
|
|
655
|
+
pathToFileURL(join(kitRootFor(appRoot()), "src/utils/url.js")).href
|
|
656
|
+
);
|
|
657
|
+
const request = {
|
|
658
|
+
kind: "load",
|
|
659
|
+
module,
|
|
660
|
+
url: event.url.href,
|
|
661
|
+
routeId: event.route.id,
|
|
662
|
+
params: {},
|
|
663
|
+
parent: await event.parent(),
|
|
664
|
+
headers,
|
|
665
|
+
...(route
|
|
666
|
+
? {
|
|
667
|
+
routePattern: route.pattern.source,
|
|
668
|
+
routeParams: route.params,
|
|
669
|
+
routePath: decode_pathname(event.url.pathname).slice(base.length) || "/",
|
|
670
|
+
}
|
|
671
|
+
: {}),
|
|
672
|
+
};
|
|
673
|
+
const raw = await invokeService(request);
|
|
674
|
+
const answer = JSON.parse(raw);
|
|
675
|
+
if (typeof answer.failure === "string") {
|
|
676
|
+
throw new Error(
|
|
677
|
+
`skgo: Go load failed during prerender: route ID ${event.route.id}, path ${event.url.pathname}, source ${source}: ${answer.failure}`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
for (const [name, values] of Object.entries(answer.headers ?? {}))
|
|
681
|
+
event.setHeaders({ [name]: values.join(", ") });
|
|
682
|
+
for (const cookie of answer.cookies ?? []) {
|
|
683
|
+
event.cookies.set(cookie.name, cookie.value, {
|
|
684
|
+
path: cookie.path,
|
|
685
|
+
...(cookie.domain ? { domain: cookie.domain } : {}),
|
|
686
|
+
...(cookie.maxAge ? { maxAge: cookie.maxAge } : {}),
|
|
687
|
+
httpOnly: cookie.httpOnly,
|
|
688
|
+
secure: cookie.secure,
|
|
689
|
+
sameSite: ["lax", "lax", "strict", "none"][cookie.sameSite] ?? "lax",
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (answer.data) {
|
|
693
|
+
const pending = new Map();
|
|
694
|
+
const revivers = {
|
|
695
|
+
...transport.decoders,
|
|
696
|
+
Promise: (id) => {
|
|
697
|
+
let resolve;
|
|
698
|
+
let reject;
|
|
699
|
+
const promise = new Promise((yes, no) => {
|
|
700
|
+
resolve = yes;
|
|
701
|
+
reject = no;
|
|
702
|
+
});
|
|
703
|
+
pending.set(id, { promise, resolve, reject });
|
|
704
|
+
return promise;
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
answer.data = devalue.unflatten(answer.data, revivers);
|
|
708
|
+
for (const chunk of answer.chunks ?? []) {
|
|
709
|
+
const deferred = pending.get(chunk.id);
|
|
710
|
+
if (!deferred) throw new Error(`skgo: unknown prerender deferred id ${chunk.id}`);
|
|
711
|
+
if (chunk.error) deferred.reject(new Error(chunk.error));
|
|
712
|
+
else deferred.resolve(devalue.unflatten(chunk.data, revivers));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return answer;
|
|
797
716
|
}
|
|
798
717
|
|
|
799
718
|
/** The emulator callback for a Go prerender remote, executing in Kit's worker. */
|
|
800
719
|
export async function remoteFunction(module, name, arg, event) {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
720
|
+
const kit = await runtime();
|
|
721
|
+
const { internal, transport } = kit;
|
|
722
|
+
const payload =
|
|
723
|
+
arg === undefined ? "" : Buffer.from(transport.stringify(arg), "utf8").toString("base64url");
|
|
724
|
+
const headers = Object.fromEntries(
|
|
725
|
+
[...event.request.headers].map(([key, value]) => [key, [value]]),
|
|
726
|
+
);
|
|
727
|
+
const raw = await invokeService({
|
|
728
|
+
kind: "remote",
|
|
729
|
+
module,
|
|
730
|
+
name,
|
|
731
|
+
payload,
|
|
732
|
+
url: event.url.href,
|
|
733
|
+
headers,
|
|
734
|
+
});
|
|
735
|
+
const answer = JSON.parse(raw);
|
|
736
|
+
if (answer.type === "error" && answer.kind === "app") throw new internal.HttpError(answer.error);
|
|
737
|
+
if (answer.type === "error" && answer.kind === "unknown") throw new Error(answer.diagnostic);
|
|
738
|
+
if (answer.type === "redirect")
|
|
739
|
+
throw new internal.Redirect(answer.redirect.status, answer.redirect.location);
|
|
740
|
+
if (answer.type !== "result" || typeof answer.data !== "string")
|
|
741
|
+
throw new Error("skgo prerender remote response is malformed");
|
|
742
|
+
const result = transport.parse(answer.data);
|
|
743
|
+
if (!result || typeof result !== "object" || !Object.hasOwn(result, "_")) {
|
|
744
|
+
throw new Error("skgo prerender remote result omitted its value member");
|
|
745
|
+
}
|
|
746
|
+
return result._;
|
|
824
747
|
}
|