@skgo/sveltekit-adapter 0.16.4 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +10 -2
- package/skgo-adapter/compat/kit-3.0.0-queue.json +9 -0
- package/skgo-adapter/compat/kit-3.0.0-queue.patch +28 -0
- package/skgo-adapter/env.js +8 -0
- package/skgo-adapter/prerender.d.ts +2 -0
- package/skgo-adapter/prerender.js +833 -0
- package/skgo-adapter.js +28 -121
package/README.md
CHANGED
|
@@ -37,6 +37,18 @@ skgo binary refuses a build written by a different one, naming both — so an
|
|
|
37
37
|
install that has fallen behind fails at startup with something a developer can
|
|
38
38
|
act on rather than as an unrelated error later.
|
|
39
39
|
|
|
40
|
+
## Kit prerender queue compatibility
|
|
41
|
+
|
|
42
|
+
Declared prerender `Inputs` require the Kit 3.0.0 queue correction shipped with
|
|
43
|
+
skgo. New projects receive the project-local patch configuration during
|
|
44
|
+
scaffolding. For an existing project, run `go tool skgo kit-patch --web web
|
|
45
|
+
--apply` from the Go application root, then install with the frontend's native
|
|
46
|
+
VitePlus command, `cd web && node_modules/.bin/vp install
|
|
47
|
+
--no-frozen-lockfile`. Finish by running `go tool skgo kit-patch --web web
|
|
48
|
+
--check` from the application root. The check verifies the actual installed
|
|
49
|
+
Kit queue source as well as the committed configuration; applying the patch
|
|
50
|
+
alone does not install dependencies.
|
|
51
|
+
|
|
40
52
|
## License
|
|
41
53
|
|
|
42
54
|
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skgo/sveltekit-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "SvelteKit adapter for skgo: builds a frontend that a Go binary renders and answers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./index.d.ts",
|
|
24
24
|
"import": "./skgo-adapter.js"
|
|
25
25
|
},
|
|
26
|
+
"./prerender": {
|
|
27
|
+
"types": "./skgo-adapter/prerender.d.ts",
|
|
28
|
+
"import": "./skgo-adapter/prerender.js"
|
|
29
|
+
},
|
|
26
30
|
"./package.json": "./package.json"
|
|
27
31
|
},
|
|
28
32
|
"types": "index.d.ts",
|
|
@@ -31,13 +35,17 @@
|
|
|
31
35
|
"skgo-adapter.js",
|
|
32
36
|
"skgo-adapter/app-paths.js",
|
|
33
37
|
"skgo-adapter/app-server.js",
|
|
38
|
+
"skgo-adapter/compat/kit-3.0.0-queue.json",
|
|
39
|
+
"skgo-adapter/compat/kit-3.0.0-queue.patch",
|
|
34
40
|
"skgo-adapter/entry.js",
|
|
35
41
|
"skgo-adapter/env-private.js",
|
|
36
42
|
"skgo-adapter/env.js",
|
|
37
43
|
"skgo-adapter/generated.js",
|
|
38
44
|
"skgo-adapter/identity.js",
|
|
39
45
|
"skgo-adapter/polyfill.js",
|
|
40
|
-
"skgo-adapter/prerender-csp.js"
|
|
46
|
+
"skgo-adapter/prerender-csp.js",
|
|
47
|
+
"skgo-adapter/prerender.js",
|
|
48
|
+
"skgo-adapter/prerender.d.ts"
|
|
41
49
|
],
|
|
42
50
|
"peerDependencies": {
|
|
43
51
|
"@sveltejs/kit": "^3.0.0"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@sveltejs/kit",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"queuePath": "src/core/postbuild/queue.js",
|
|
5
|
+
"stockSHA256": "dbe8bbacab35cbd119d6f5bcf9b527ce0f7a1bfa63954b5ebe047df49f46c6d5",
|
|
6
|
+
"correctedSHA256": "400bf34544e2b3c5512e489eb5a0ca99a7483f0b6d733f70cab9e81e28f3a0f5",
|
|
7
|
+
"patchSHA256": "0d35d370d3e5fb6e0c801cd1079013b3d487d6e301e27777f26fabbf6e0106c4",
|
|
8
|
+
"correctionID": "skgo-kit-3.0.0-prerender-queue-v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
diff --git a/src/core/postbuild/queue.js b/src/core/postbuild/queue.js
|
|
2
|
+
--- a/src/core/postbuild/queue.js
|
|
3
|
+
+++ b/src/core/postbuild/queue.js
|
|
4
|
+
@@ -16,6 +16,7 @@
|
|
5
|
+
|
|
6
|
+
let current = 0;
|
|
7
|
+
let closed = false;
|
|
8
|
+
+ let done_requested = false;
|
|
9
|
+
|
|
10
|
+
promise.catch(() => {
|
|
11
|
+
// this is necessary in case a catch handler is never added
|
|
12
|
+
@@ -39,7 +40,7 @@
|
|
13
|
+
current -= 1;
|
|
14
|
+
dequeue();
|
|
15
|
+
});
|
|
16
|
+
- } else if (current === 0) {
|
|
17
|
+
+ } else if (current === 0 && done_requested) {
|
|
18
|
+
closed = true;
|
|
19
|
+
resolve();
|
|
20
|
+
}
|
|
21
|
+
@@ -61,6 +62,7 @@
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
done: () => {
|
|
25
|
+
+ done_requested = true;
|
|
26
|
+
if (current === 0) {
|
|
27
|
+
closed = true;
|
|
28
|
+
resolve();
|
package/skgo-adapter/env.js
CHANGED
|
@@ -90,6 +90,7 @@ const kitSync = await import(pathToFileURL(join(kitRoot, 'src/core/sync/sync.js'
|
|
|
90
90
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
91
91
|
const ENTRY = join(here, 'entry.js');
|
|
92
92
|
const APP_SERVER = join(here, 'app-server.js');
|
|
93
|
+
const PRERENDER_HELPER = '@skgo/sveltekit-adapter/prerender';
|
|
93
94
|
const APP_PATHS = join(here, 'app-paths.js');
|
|
94
95
|
const POLYFILL = join(here, 'polyfill.js');
|
|
95
96
|
|
|
@@ -211,6 +212,10 @@ export function gojaEnvironment() {
|
|
|
211
212
|
// `get_hooks` carries `reroute` and nothing else the engine needs; Go
|
|
212
213
|
// does the routing.
|
|
213
214
|
'skgo:generated': () => 'export const get_hooks = () => ({});',
|
|
215
|
+
// The Goja renderer cannot call back into the build-time process owner.
|
|
216
|
+
// Keep this module inert so its Node process dependencies never enter the
|
|
217
|
+
// renderer bundle.
|
|
218
|
+
'skgo:prerender': () => 'export async function remoteInputs() { throw new Error("skgo: declared prerender inputs are build-only"); }',
|
|
214
219
|
// Neither kit nor Svelte can reach AsyncLocalStorage here, and neither
|
|
215
220
|
// needs to: the webcontainer flag in the banner selects Svelte's own
|
|
216
221
|
// supported fallback — a module-global render context and a serialised
|
|
@@ -302,6 +307,7 @@ export function gojaEnvironment() {
|
|
|
302
307
|
resolveId: {
|
|
303
308
|
order: 'pre',
|
|
304
309
|
async handler(id, importer) {
|
|
310
|
+
if (id === PRERENDER_HELPER) return PREFIX + 'skgo:prerender';
|
|
305
311
|
if (id in sources) return PREFIX + id;
|
|
306
312
|
if (id.startsWith(PREFIX)) return id;
|
|
307
313
|
if (id in aliases) return aliases[id];
|
|
@@ -667,6 +673,7 @@ export function gojaDevEnvironment({ outDir = '.svelte-kit' } = {}) {
|
|
|
667
673
|
'skgo:hooks': () => hooksModule(root),
|
|
668
674
|
'skgo:esm-env': () => 'export const DEV = true; export const BROWSER = false;',
|
|
669
675
|
'skgo:generated': () => 'export const get_hooks = () => ({});',
|
|
676
|
+
'skgo:prerender': () => 'export async function remoteInputs() { throw new Error("skgo: declared prerender inputs are build-only"); }',
|
|
670
677
|
'skgo:missing': () =>
|
|
671
678
|
'throw new Error("skgo: node:async_hooks is unavailable in the SSR engine");'
|
|
672
679
|
};
|
|
@@ -944,6 +951,7 @@ export function gojaDevEnvironment({ outDir = '.svelte-kit' } = {}) {
|
|
|
944
951
|
resolveId: {
|
|
945
952
|
order: 'pre',
|
|
946
953
|
async handler(id, importer) {
|
|
954
|
+
if (id === PRERENDER_HELPER) return PREFIX + 'skgo:prerender';
|
|
947
955
|
if (id in sources) return PREFIX + id;
|
|
948
956
|
if (id.startsWith(PREFIX)) return id;
|
|
949
957
|
if (id in aliases) return aliases[id];
|
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { isMainThread, parentPort } from 'node:worker_threads';
|
|
10
|
+
|
|
11
|
+
const TAG = 'skgo-prerender-v1';
|
|
12
|
+
const owners = new Map();
|
|
13
|
+
const requestWaiters = new Map();
|
|
14
|
+
const ownerByRoot = new Map();
|
|
15
|
+
let parentPortListening = false;
|
|
16
|
+
|
|
17
|
+
function appRoot() {
|
|
18
|
+
return resolve(process.cwd());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function kitRootFor(root) {
|
|
22
|
+
return realpathSync(join(root, 'node_modules/@sveltejs/kit'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function kitQueueFailure(detail) {
|
|
26
|
+
const error = makeError(`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".`);
|
|
27
|
+
error.name = 'SKGO_KIT_PRERENDER_QUEUE';
|
|
28
|
+
error.code = 'SKGO_KIT_PRERENDER_QUEUE';
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function verifyKitQueue(root) {
|
|
33
|
+
let metadata;
|
|
34
|
+
try {
|
|
35
|
+
const appRequire = createRequire(join(root, 'package.json'));
|
|
36
|
+
const adapterPackage = appRequire.resolve('@skgo/sveltekit-adapter/package.json');
|
|
37
|
+
metadata = JSON.parse(readFileSync(join(dirname(adapterPackage), 'skgo-adapter/compat/kit-3.0.0-queue.json'), 'utf8'));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw kitQueueFailure(`adapter compatibility metadata could not be read: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const kitRoot = kitRootFor(root);
|
|
43
|
+
const pkg = JSON.parse(readFileSync(join(kitRoot, 'package.json'), 'utf8'));
|
|
44
|
+
const queuePath = join(kitRoot, metadata.queuePath);
|
|
45
|
+
const queue = readFileSync(queuePath);
|
|
46
|
+
const digest = createHash('sha256').update(queue).digest('hex');
|
|
47
|
+
if (pkg.name !== metadata.package || pkg.version !== metadata.version || digest !== metadata.correctedSHA256) {
|
|
48
|
+
throw kitQueueFailure(`resolved ${pkg.name ?? 'unknown'}@${pkg.version ?? 'unknown'} queue SHA-256 ${digest}; expected ${metadata.package}@${metadata.version} with SHA-256 ${metadata.correctedSHA256}`);
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error?.code === 'SKGO_KIT_PRERENDER_QUEUE') throw error;
|
|
52
|
+
throw kitQueueFailure(`resolved Kit queue could not be verified: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function generatedConfig(root) {
|
|
57
|
+
const manifest = JSON.parse(readFileSync(join(root, 'skgo.remotes.json'), 'utf8'));
|
|
58
|
+
if (!manifest.prerender?.root || !manifest.prerender?.package) {
|
|
59
|
+
throw new Error('skgo: generated prerender command is missing');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
cwd: resolve(root, manifest.prerender.root),
|
|
63
|
+
pkg: manifest.prerender.package
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function vitePlusBuildParent(root) {
|
|
68
|
+
if (!isMainThread || process.env.NODE_PACKAGE_MANAGER !== 'vite-plus' || process.argv[2] !== 'build' || !process.argv[1]) return null;
|
|
69
|
+
try {
|
|
70
|
+
const appRequire = createRequire(join(root, 'package.json'));
|
|
71
|
+
const vpRequire = createRequire(appRequire.resolve('vite-plus/package.json'));
|
|
72
|
+
const expected = realpathSync(join(dirname(vpRequire.resolve('vite')), 'cli.js'));
|
|
73
|
+
return realpathSync(process.argv[1]) === expected && process.ppid > 1 ? process.ppid : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeError(message) {
|
|
80
|
+
const error = new Error(message);
|
|
81
|
+
return error;
|
|
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 {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validateEnvelope(request, raw) {
|
|
91
|
+
let value;
|
|
92
|
+
try { value = JSON.parse(raw); }
|
|
93
|
+
catch (error) { throw makeError(`skgo prerender command returned malformed JSON: ${error.message}`); }
|
|
94
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw makeError('skgo prerender command returned a non-object response');
|
|
95
|
+
if (request.kind === 'remote-inputs') {
|
|
96
|
+
if (typeof value.inputs !== 'string') throw makeError('skgo prerender inputs response has no devalue payload');
|
|
97
|
+
} else if (request.kind === 'remote') {
|
|
98
|
+
if (value.type === 'result' && typeof value.data === 'string') return;
|
|
99
|
+
if (value.type === 'error' && value.kind === 'app' && value.error && Number.isInteger(value.error.status) && typeof value.error.message === 'string') return;
|
|
100
|
+
if (value.type === 'error' && value.kind === 'unknown' && value.error && Number.isInteger(value.error.status) && typeof value.error.message === 'string' && typeof value.diagnostic === 'string') return;
|
|
101
|
+
if (value.type === 'redirect' && Number.isInteger(value.redirect?.status) && value.redirect.status >= 300 && value.redirect.status <= 308 && typeof value.redirect.location === 'string') return;
|
|
102
|
+
throw makeError('skgo prerender remote response has an invalid envelope');
|
|
103
|
+
} else if (request.kind === 'load') {
|
|
104
|
+
const allowed = ['data', 'chunks', 'error', 'failure', 'redirect', 'headers', 'cookies'];
|
|
105
|
+
if (!allowed.some((key) => key in value)) throw makeError('skgo prerender load response has no recognized fields');
|
|
106
|
+
if (value.data !== undefined && typeof value.data !== 'string' && !(value.data && typeof value.data === 'object')) throw makeError('skgo prerender load response has invalid data');
|
|
107
|
+
if (value.failure !== undefined && typeof value.failure !== 'string') throw makeError('skgo prerender load response has invalid failure');
|
|
108
|
+
} else {
|
|
109
|
+
throw makeError(`skgo prerender owner received unsupported request kind ${request.kind}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function groupAlive(job) {
|
|
114
|
+
if (process.platform === 'win32' || !job.group) return false;
|
|
115
|
+
try {
|
|
116
|
+
process.kill(-job.group, 0);
|
|
117
|
+
return true;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error?.code === 'ESRCH') return false;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function signalTree(job, signal) {
|
|
125
|
+
if (process.platform === 'win32') {
|
|
126
|
+
if (!job.pid) return;
|
|
127
|
+
try {
|
|
128
|
+
const killer = spawn('taskkill', ['/PID', String(job.pid), '/T', '/F'], {
|
|
129
|
+
stdio: 'ignore',
|
|
130
|
+
windowsHide: true
|
|
131
|
+
});
|
|
132
|
+
job.killers.add(killer);
|
|
133
|
+
killer.once('close', () => job.killers.delete(killer));
|
|
134
|
+
killer.once('error', () => job.killers.delete(killer));
|
|
135
|
+
} catch {
|
|
136
|
+
// The direct child is still retained and will be awaited below.
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!job.group) return;
|
|
141
|
+
try {
|
|
142
|
+
process.kill(-job.group, signal);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error?.code !== 'ESRCH') throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function stopGroup(job, graceMs = 150) {
|
|
149
|
+
if (process.platform === 'win32') {
|
|
150
|
+
signalTree(job, 'SIGKILL');
|
|
151
|
+
await Promise.allSettled([...job.killers].map((killer) => new Promise((resolve) => killer.once('close', resolve))));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!groupAlive(job)) return;
|
|
155
|
+
signalTree(job, 'SIGTERM');
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
157
|
+
if (groupAlive(job)) signalTree(job, 'SIGKILL');
|
|
158
|
+
const deadline = Date.now() + 5000;
|
|
159
|
+
while (groupAlive(job) && Date.now() < deadline) {
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
161
|
+
}
|
|
162
|
+
if (groupAlive(job)) throw makeError(`skgo could not drain process group ${job.group}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function beginTermination(job) {
|
|
166
|
+
try { signalTree(job, 'SIGTERM'); } catch {}
|
|
167
|
+
if (process.platform === 'win32') return Promise.resolve();
|
|
168
|
+
return new Promise((resolve) => setTimeout(() => {
|
|
169
|
+
if (groupAlive(job)) {
|
|
170
|
+
try { signalTree(job, 'SIGKILL'); } catch {}
|
|
171
|
+
}
|
|
172
|
+
resolve();
|
|
173
|
+
}, 150));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function startDrain(job) {
|
|
177
|
+
if (!job.drainPromise) job.drainPromise = beginTermination(job);
|
|
178
|
+
return job.drainPromise;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function createJob(session, command, args, options = {}) {
|
|
182
|
+
const detached = process.platform !== 'win32';
|
|
183
|
+
const child = spawn(command, args, {
|
|
184
|
+
cwd: options.cwd,
|
|
185
|
+
env: options.env ?? process.env,
|
|
186
|
+
stdio: options.input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'],
|
|
187
|
+
detached,
|
|
188
|
+
windowsHide: true
|
|
189
|
+
});
|
|
190
|
+
const job = {
|
|
191
|
+
child,
|
|
192
|
+
pid: child.pid,
|
|
193
|
+
group: detached ? child.pid : null,
|
|
194
|
+
killers: new Set(),
|
|
195
|
+
stdout: '',
|
|
196
|
+
stderr: '',
|
|
197
|
+
closed: false,
|
|
198
|
+
settled: false,
|
|
199
|
+
closePromise: null,
|
|
200
|
+
resolveClose: null,
|
|
201
|
+
drainPromise: null,
|
|
202
|
+
done: null,
|
|
203
|
+
resolve: null,
|
|
204
|
+
reject: null
|
|
205
|
+
};
|
|
206
|
+
session.jobs.add(job);
|
|
207
|
+
session.groups.add(job);
|
|
208
|
+
job.done = new Promise((resolve, reject) => {
|
|
209
|
+
job.resolve = resolve;
|
|
210
|
+
job.reject = reject;
|
|
211
|
+
});
|
|
212
|
+
job.closePromise = new Promise((resolve) => { job.resolveClose = resolve; });
|
|
213
|
+
job.done.catch(() => {});
|
|
214
|
+
const settle = (error, value) => {
|
|
215
|
+
if (job.settled) return;
|
|
216
|
+
job.settled = true;
|
|
217
|
+
if (error) job.reject(error);
|
|
218
|
+
else job.resolve(value);
|
|
219
|
+
};
|
|
220
|
+
const pipeError = (label) => (error) => {
|
|
221
|
+
settle(makeError(`skgo prerender ${label} pipe failed: ${error.message}`));
|
|
222
|
+
signalTree(job, 'SIGTERM');
|
|
223
|
+
};
|
|
224
|
+
child.on('error', (error) => {
|
|
225
|
+
settle(makeError(`skgo prerender process failed to start: ${error.message}`));
|
|
226
|
+
});
|
|
227
|
+
// Do not wait for inherited stdout/stderr pipes to close before starting
|
|
228
|
+
// descendant termination: a child which inherited either pipe can hold the
|
|
229
|
+
// leader's `close` event open after the Go process itself has exited.
|
|
230
|
+
child.on('exit', () => { void startDrain(job); });
|
|
231
|
+
child.stdout.setEncoding('utf8');
|
|
232
|
+
child.stderr.setEncoding('utf8');
|
|
233
|
+
child.stdout.on('data', (chunk) => { job.stdout += chunk; });
|
|
234
|
+
child.stderr.on('data', (chunk) => { job.stderr += chunk; });
|
|
235
|
+
child.stdin?.on('error', pipeError('stdin'));
|
|
236
|
+
child.stdout.on('error', pipeError('stdout'));
|
|
237
|
+
child.stderr.on('error', pipeError('stderr'));
|
|
238
|
+
child.stdin?.on('close', () => {
|
|
239
|
+
if (options.input !== undefined && !job.closed && !child.stdin.writableFinished) {
|
|
240
|
+
settle(makeError('skgo prerender process closed stdin before accepting its request'));
|
|
241
|
+
signalTree(job, 'SIGTERM');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
child.on('close', async (code, signal) => {
|
|
245
|
+
job.closed = true;
|
|
246
|
+
const exit = { code, signal, stdout: job.stdout, stderr: job.stderr };
|
|
247
|
+
let treeError;
|
|
248
|
+
// A successful leader can leave compiler or application descendants in
|
|
249
|
+
// its process group. Drain them before any request or outer build settles.
|
|
250
|
+
await startDrain(job);
|
|
251
|
+
await stopGroup(job).catch((error) => { treeError = error; });
|
|
252
|
+
session.jobs.delete(job);
|
|
253
|
+
if (!treeError) session.groups.delete(job);
|
|
254
|
+
job.resolveClose();
|
|
255
|
+
if (treeError) {
|
|
256
|
+
settle(treeError);
|
|
257
|
+
} else if (!job.settled) {
|
|
258
|
+
if (code !== 0) {
|
|
259
|
+
settle(makeError(`skgo prerender command exited ${code ?? signal}: ${job.stderr.trim()}`));
|
|
260
|
+
} else if (options.request) {
|
|
261
|
+
try {
|
|
262
|
+
validateEnvelope(options.request, job.stdout);
|
|
263
|
+
settle(null, exit);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
settle(makeError(`${error.message}; ${job.stderr.trim()}`));
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
settle(null, exit);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
if (options.input !== undefined) {
|
|
273
|
+
// All child and pipe listeners exist before writing the request.
|
|
274
|
+
child.stdin.end(options.input);
|
|
275
|
+
}
|
|
276
|
+
return job;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function makeSession(root, ownerParentPid) {
|
|
280
|
+
const session = {
|
|
281
|
+
id: randomUUID(),
|
|
282
|
+
root,
|
|
283
|
+
cwd: null,
|
|
284
|
+
pkg: null,
|
|
285
|
+
dir: null,
|
|
286
|
+
binary: null,
|
|
287
|
+
compilePromise: null,
|
|
288
|
+
jobs: new Set(),
|
|
289
|
+
groups: new Set(),
|
|
290
|
+
workers: new Map(),
|
|
291
|
+
observedWorkers: new Map(),
|
|
292
|
+
closing: false,
|
|
293
|
+
failed: null,
|
|
294
|
+
cleanupPromise: null,
|
|
295
|
+
ownerParentPid,
|
|
296
|
+
parentObserver: null,
|
|
297
|
+
parentLossStarted: false,
|
|
298
|
+
signalHandlers: new Map(),
|
|
299
|
+
watchParent() {
|
|
300
|
+
if (session.ownerParentPid === null || session.parentObserver) return;
|
|
301
|
+
session.parentObserver = setInterval(() => {
|
|
302
|
+
if (process.ppid === session.ownerParentPid && process.ppid > 1) return;
|
|
303
|
+
clearInterval(session.parentObserver);
|
|
304
|
+
session.parentObserver = null;
|
|
305
|
+
if (session.parentLossStarted) return;
|
|
306
|
+
session.parentLossStarted = true;
|
|
307
|
+
void (async () => {
|
|
308
|
+
try {
|
|
309
|
+
await session.fail(makeError('skgo prerender Vite build parent exited'));
|
|
310
|
+
for (const [signal, handler] of session.signalHandlers) process.off(signal, handler);
|
|
311
|
+
session.signalHandlers.clear();
|
|
312
|
+
process.exitCode = 1;
|
|
313
|
+
try { process.kill(process.pid, 'SIGTERM'); }
|
|
314
|
+
catch { process.exit(1); }
|
|
315
|
+
} catch (error) {
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
reportOwnerFailure(error);
|
|
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. */
|
|
534
|
+
export function createPrerenderOwner(root = appRoot()) {
|
|
535
|
+
if (!isMainThread) return null;
|
|
536
|
+
root = resolve(root);
|
|
537
|
+
const existing = ownerByRoot.get(root);
|
|
538
|
+
if (existing) return existing;
|
|
539
|
+
const session = makeSession(root, vitePlusBuildParent(root));
|
|
540
|
+
ownerByRoot.set(root, session);
|
|
541
|
+
session.watchParent();
|
|
542
|
+
session.onWorker = (worker) => {
|
|
543
|
+
const listener = (message) => {
|
|
544
|
+
if (!message || message.tag !== TAG || message.type !== 'connect' || message.appRoot !== root) return;
|
|
545
|
+
if (message.session !== undefined && message.session !== session.id) return;
|
|
546
|
+
void connectWorker(session, worker, message).catch(reportOwnerFailure);
|
|
547
|
+
};
|
|
548
|
+
session.observedWorkers.set(worker, listener);
|
|
549
|
+
worker.on('message', listener);
|
|
550
|
+
};
|
|
551
|
+
process.on('worker', session.onWorker);
|
|
552
|
+
return session;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Wrap Vite's whole native app-build boundary so its caller cannot reject before Go drains. */
|
|
556
|
+
export function installPrerenderFailureBoundary(config, owner) {
|
|
557
|
+
if (!owner || typeof config?.getSortedPlugins !== 'function') return;
|
|
558
|
+
const boundary = owner.boundary ??= {
|
|
559
|
+
seenPlugins: new WeakSet(),
|
|
560
|
+
wrappedHandlers: new WeakMap(),
|
|
561
|
+
wrappers: new WeakSet()
|
|
562
|
+
};
|
|
563
|
+
const wrap = (handler) => {
|
|
564
|
+
if (boundary.wrappers.has(handler)) return handler;
|
|
565
|
+
const existing = boundary.wrappedHandlers.get(handler);
|
|
566
|
+
if (existing) return existing;
|
|
567
|
+
const wrapped = async function (...args) {
|
|
568
|
+
let result;
|
|
569
|
+
try {
|
|
570
|
+
result = await handler.apply(this, args);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
try {
|
|
573
|
+
await owner.fail(error);
|
|
574
|
+
} catch (failure) {
|
|
575
|
+
const cleanupFailure = owner.cleanupFailure ?? failure?.cleanupError ?? failure;
|
|
576
|
+
if (error && typeof error === 'object') {
|
|
577
|
+
try { error.cleanupError = cleanupFailure; }
|
|
578
|
+
catch { reportOwnerFailure({ message: 'could not attach prerender cleanup failure to the native build error', cleanupError: cleanupFailure }); }
|
|
579
|
+
} else {
|
|
580
|
+
reportOwnerFailure({ message: `native build error: ${String(error)}`, cleanupError: cleanupFailure });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
if (owner.failed) {
|
|
586
|
+
try { await owner.cleanup(); }
|
|
587
|
+
catch (cleanupError) {
|
|
588
|
+
if (typeof owner.failed === 'object') owner.failed.cleanupError = cleanupError;
|
|
589
|
+
}
|
|
590
|
+
throw owner.failed;
|
|
591
|
+
}
|
|
592
|
+
return result;
|
|
593
|
+
};
|
|
594
|
+
boundary.wrappedHandlers.set(handler, wrapped);
|
|
595
|
+
boundary.wrappers.add(wrapped);
|
|
596
|
+
return wrapped;
|
|
597
|
+
};
|
|
598
|
+
const setHook = (target, replacement) => {
|
|
599
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, 'buildApp') ?? {
|
|
600
|
+
configurable: true, enumerable: true, writable: true
|
|
601
|
+
};
|
|
602
|
+
Object.defineProperty(target, 'buildApp', { ...descriptor, value: replacement });
|
|
603
|
+
};
|
|
604
|
+
for (const plugin of config.getSortedPlugins('buildApp')) {
|
|
605
|
+
if (!plugin || typeof plugin !== 'object' || boundary.seenPlugins.has(plugin)) continue;
|
|
606
|
+
boundary.seenPlugins.add(plugin);
|
|
607
|
+
const hook = plugin.buildApp;
|
|
608
|
+
if (typeof hook === 'function') {
|
|
609
|
+
setHook(plugin, wrap(hook));
|
|
610
|
+
} else if (hook && typeof hook.handler === 'function') {
|
|
611
|
+
const descriptors = Object.getOwnPropertyDescriptors(hook);
|
|
612
|
+
descriptors.handler = { ...descriptors.handler, value: wrap(hook.handler) };
|
|
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;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function kitRuntime(root) {
|
|
705
|
+
const kitRoot = kitRootFor(root);
|
|
706
|
+
const transportURL = pathToFileURL(join(kitRoot, 'src/runtime/app/internal/transport.js')).href;
|
|
707
|
+
const internalURL = pathToFileURL(join(kitRoot, 'src/exports/internal/shared.js')).href;
|
|
708
|
+
const transport = await import(transportURL);
|
|
709
|
+
const hooks = join(root, '.svelte-kit/output/server/entries/hooks.universal.js');
|
|
710
|
+
let definitions = {};
|
|
711
|
+
try { ({ transport: definitions = {} } = await import(pathToFileURL(hooks).href)); }
|
|
712
|
+
catch (error) {
|
|
713
|
+
if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
|
|
714
|
+
}
|
|
715
|
+
transport.init_transport(definitions);
|
|
716
|
+
const internal = await import(internalURL);
|
|
717
|
+
const require = createRequire(join(kitRoot, 'package.json'));
|
|
718
|
+
const devalue = await import(pathToFileURL(require.resolve('devalue')).href);
|
|
719
|
+
return { transport, internal, devalue };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let runtimePromise;
|
|
723
|
+
function runtime() {
|
|
724
|
+
if (!runtimePromise) runtimePromise = kitRuntime(appRoot());
|
|
725
|
+
return runtimePromise;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** Called by the generated native Kit input producer. */
|
|
729
|
+
export async function remoteInputs(module, name) {
|
|
730
|
+
try { verifyKitQueue(appRoot()); }
|
|
731
|
+
catch (error) { return reportWorkerFatal(error); }
|
|
732
|
+
const raw = await invokeWorker({ kind: 'remote-inputs', module, name });
|
|
733
|
+
try {
|
|
734
|
+
const answer = JSON.parse(raw);
|
|
735
|
+
if (typeof answer.inputs !== 'string') throw makeError('skgo prerender inputs response has no devalue payload');
|
|
736
|
+
const { transport } = await runtime();
|
|
737
|
+
const values = transport.parse(answer.inputs);
|
|
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
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** The emulator callback for a Go load, executing entirely in its Kit worker. */
|
|
746
|
+
export async function remoteLoad(module, source, event) {
|
|
747
|
+
let kit;
|
|
748
|
+
try { kit = await runtime(); } catch (error) { return reportWorkerFatal(error); }
|
|
749
|
+
const { devalue, transport } = kit;
|
|
750
|
+
const headers = Object.fromEntries([...event.request.headers].map(([name, value]) => [name, [value]]));
|
|
751
|
+
const cookies = event.cookies.getAll();
|
|
752
|
+
if (cookies.length) {
|
|
753
|
+
headers.cookie = [cookies.map(({ name, value }) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join('; ')];
|
|
754
|
+
}
|
|
755
|
+
// Kit's JS matcher results cannot construct named Go values. Send its
|
|
756
|
+
// actual route metadata and the unconverted path to the build-only Go load.
|
|
757
|
+
const { manifest } = await import(pathToFileURL(join(appRoot(), '.svelte-kit/output/server/manifest-full.js')).href);
|
|
758
|
+
const route = manifest.routes.find((candidate) => candidate.id === event.route.id);
|
|
759
|
+
const prefix = manifest.app_path.slice(0, -manifest.app_dir.length);
|
|
760
|
+
const base = prefix ? '/' + prefix.slice(0, -1) : '';
|
|
761
|
+
const { decode_pathname } = await import(pathToFileURL(join(kitRootFor(appRoot()), 'src/utils/url.js')).href);
|
|
762
|
+
const request = {
|
|
763
|
+
kind: 'load', module, url: event.url.href, routeId: event.route.id,
|
|
764
|
+
params: {}, parent: await event.parent(), headers,
|
|
765
|
+
...(route ? { routePattern: route.pattern.source, routeParams: route.params,
|
|
766
|
+
routePath: decode_pathname(event.url.pathname).slice(base.length) || '/' } : {})
|
|
767
|
+
};
|
|
768
|
+
const raw = await invokeWorker(request);
|
|
769
|
+
const answer = JSON.parse(raw);
|
|
770
|
+
if (typeof answer.failure === 'string') {
|
|
771
|
+
throw new Error(`skgo: Go load failed during prerender: route ID ${event.route.id}, path ${event.url.pathname}, source ${source}: ${answer.failure}`);
|
|
772
|
+
}
|
|
773
|
+
for (const [name, values] of Object.entries(answer.headers ?? {})) event.setHeaders({ [name]: values.join(', ') });
|
|
774
|
+
for (const cookie of answer.cookies ?? []) {
|
|
775
|
+
event.cookies.set(cookie.name, cookie.value, {
|
|
776
|
+
path: cookie.path, ...(cookie.domain ? { domain: cookie.domain } : {}),
|
|
777
|
+
...(cookie.maxAge ? { maxAge: cookie.maxAge } : {}), httpOnly: cookie.httpOnly,
|
|
778
|
+
secure: cookie.secure, sameSite: ['lax', 'lax', 'strict', 'none'][cookie.sameSite] ?? 'lax'
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
if (answer.data) {
|
|
782
|
+
const pending = new Map();
|
|
783
|
+
const revivers = {
|
|
784
|
+
...transport.decoders,
|
|
785
|
+
Promise: (id) => {
|
|
786
|
+
let resolve;
|
|
787
|
+
let reject;
|
|
788
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
789
|
+
pending.set(id, { promise, resolve, reject });
|
|
790
|
+
return promise;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
try {
|
|
794
|
+
answer.data = devalue.unflatten(answer.data, revivers);
|
|
795
|
+
for (const chunk of answer.chunks ?? []) {
|
|
796
|
+
const deferred = pending.get(chunk.id);
|
|
797
|
+
if (!deferred) throw makeError(`skgo: unknown prerender deferred id ${chunk.id}`);
|
|
798
|
+
if (chunk.error) deferred.reject(new Error(chunk.error));
|
|
799
|
+
else deferred.resolve(devalue.unflatten(chunk.data, revivers));
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
return reportWorkerFatal(error);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return answer;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** The emulator callback for a Go prerender remote, executing in Kit's worker. */
|
|
809
|
+
export async function remoteFunction(module, name, arg, event) {
|
|
810
|
+
let kit;
|
|
811
|
+
try { kit = await runtime(); } catch (error) { return reportWorkerFatal(error); }
|
|
812
|
+
const { internal, transport } = kit;
|
|
813
|
+
let payload;
|
|
814
|
+
try { payload = arg === undefined ? '' : Buffer.from(transport.stringify(arg), 'utf8').toString('base64url'); }
|
|
815
|
+
catch (error) { return reportWorkerFatal(error); }
|
|
816
|
+
const headers = Object.fromEntries([...event.request.headers].map(([key, value]) => [key, [value]]));
|
|
817
|
+
const raw = await invokeWorker({
|
|
818
|
+
kind: 'remote', module, name, payload, url: event.url.href, headers
|
|
819
|
+
});
|
|
820
|
+
const answer = JSON.parse(raw);
|
|
821
|
+
if (answer.type === 'error' && answer.kind === 'app') throw new internal.HttpError(answer.error);
|
|
822
|
+
if (answer.type === 'error' && answer.kind === 'unknown') throw new Error(answer.diagnostic);
|
|
823
|
+
if (answer.type === 'redirect') throw new internal.Redirect(answer.redirect.status, answer.redirect.location);
|
|
824
|
+
if (answer.type !== 'result' || typeof answer.data !== 'string') throw makeError('skgo prerender remote response is malformed');
|
|
825
|
+
try {
|
|
826
|
+
const result = transport.parse(answer.data);
|
|
827
|
+
if (!result || typeof result !== 'object' || !Object.hasOwn(result, '_')) {
|
|
828
|
+
throw makeError('skgo prerender remote result omitted its value member');
|
|
829
|
+
}
|
|
830
|
+
return result._;
|
|
831
|
+
}
|
|
832
|
+
catch (error) { return reportWorkerFatal(error); }
|
|
833
|
+
}
|
package/skgo-adapter.js
CHANGED
|
@@ -12,34 +12,20 @@ import {
|
|
|
12
12
|
writeFileSync
|
|
13
13
|
} from 'node:fs';
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import { spawn } from 'node:child_process';
|
|
16
|
-
import { createRequire } from 'node:module';
|
|
17
|
-
import { tmpdir } from 'node:os';
|
|
18
15
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
19
16
|
import { pathToFileURL } from 'node:url';
|
|
17
|
+
import {
|
|
18
|
+
createPrerenderOwner,
|
|
19
|
+
installPrerenderFailureBoundary,
|
|
20
|
+
joinFailedPrerenderOwner,
|
|
21
|
+
remoteFunction,
|
|
22
|
+
remoteLoad
|
|
23
|
+
} from './skgo-adapter/prerender.js';
|
|
20
24
|
import { gojaDevEnvironment, gojaDevUnchangedFiles, gojaEnvironment, goEnvironmentValues, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
|
|
21
25
|
import { identity } from './skgo-adapter/identity.js';
|
|
22
26
|
import { checkEndpoints, validateGenerated } from './skgo-adapter/generated.js';
|
|
23
27
|
import { authorizePrerenderedScripts } from './skgo-adapter/prerender-csp.js';
|
|
24
28
|
|
|
25
|
-
function runPrerenderCommand(command, args, cwd, input = '') {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
28
|
-
let stdout = '';
|
|
29
|
-
let stderr = '';
|
|
30
|
-
child.stdout.setEncoding('utf8');
|
|
31
|
-
child.stderr.setEncoding('utf8');
|
|
32
|
-
child.stdout.on('data', (chunk) => (stdout += chunk));
|
|
33
|
-
child.stderr.on('data', (chunk) => (stderr += chunk));
|
|
34
|
-
child.on('error', reject);
|
|
35
|
-
child.on('close', (code) => {
|
|
36
|
-
if (code === 0) resolve(stdout);
|
|
37
|
-
else reject(new Error(`skgo prerender command exited ${code}: ${stderr}`));
|
|
38
|
-
});
|
|
39
|
-
child.stdin.end(input);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
29
|
// Which skgo this adapter is: the version this package was published at, and a
|
|
44
30
|
// fingerprint taken over its own files. The Go that reads the manifest below
|
|
45
31
|
// takes the same fingerprint over the copy it embeds and refuses a build whose
|
|
@@ -83,33 +69,21 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
83
69
|
// build itself waits until adapt() knows the node table.
|
|
84
70
|
const goja = gojaEnvironment();
|
|
85
71
|
const environment = goEnvironmentValues(out);
|
|
86
|
-
let
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
prerenderBinary = join(dir, 'build');
|
|
100
|
-
await runPrerenderCommand('go', ['build', '-o', prerenderBinary, generated.prerender.package], prerenderRoot);
|
|
101
|
-
process.on('exit', () => rmSync(dir, { recursive: true, force: true }));
|
|
102
|
-
const fromKit = createRequire(realpathSync(join(process.cwd(), 'node_modules/@sveltejs/kit/package.json')));
|
|
103
|
-
({ unflatten, stringify } = await import(pathToFileURL(fromKit.resolve('devalue')).href));
|
|
104
|
-
const hooks = join(process.cwd(), '.svelte-kit/output/server/entries/hooks.universal.js');
|
|
105
|
-
if (existsSync(hooks)) {
|
|
106
|
-
const { transport = {} } = await import(pathToFileURL(hooks).href);
|
|
107
|
-
transportDecoders = Object.fromEntries(Object.entries(transport).map(([key, entry]) => [key, entry.decode]));
|
|
108
|
-
}
|
|
109
|
-
})();
|
|
72
|
+
let prerenderOwner;
|
|
73
|
+
const prerenderOwnerPlugin = {
|
|
74
|
+
name: 'skgo-prerender-main-owner',
|
|
75
|
+
apply: 'build',
|
|
76
|
+
configResolved(config) {
|
|
77
|
+
prerenderOwner = createPrerenderOwner(process.cwd());
|
|
78
|
+
installPrerenderFailureBoundary(config, prerenderOwner);
|
|
79
|
+
},
|
|
80
|
+
buildEnd(error) {
|
|
81
|
+
if (error && prerenderOwner) return prerenderOwner.fail(error);
|
|
82
|
+
},
|
|
83
|
+
async closeBundle() {
|
|
84
|
+
await joinFailedPrerenderOwner(prerenderOwner);
|
|
110
85
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
86
|
+
};
|
|
113
87
|
|
|
114
88
|
return {
|
|
115
89
|
name: 'skgo',
|
|
@@ -117,75 +91,10 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
117
91
|
return {
|
|
118
92
|
platform: () => ({
|
|
119
93
|
async skgoPrerenderLoad(module, source, event) {
|
|
120
|
-
|
|
121
|
-
const headers = Object.fromEntries(
|
|
122
|
-
[...event.request.headers].map(([name, value]) => [name, [value]])
|
|
123
|
-
);
|
|
124
|
-
const cookies = event.cookies.getAll();
|
|
125
|
-
if (cookies.length) {
|
|
126
|
-
headers.cookie = [cookies.map(({ name, value }) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join('; ')];
|
|
127
|
-
}
|
|
128
|
-
const request = {
|
|
129
|
-
kind: 'load',
|
|
130
|
-
module,
|
|
131
|
-
url: event.url.href,
|
|
132
|
-
routeId: event.route.id,
|
|
133
|
-
params: Object.fromEntries(Object.entries(event.params)),
|
|
134
|
-
parent: await event.parent(),
|
|
135
|
-
headers
|
|
136
|
-
};
|
|
137
|
-
const raw = await runPrerenderCommand(prerenderBinary, [], prerenderRoot, JSON.stringify(request));
|
|
138
|
-
const answer = JSON.parse(raw);
|
|
139
|
-
if (typeof answer.failure === 'string') {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`skgo: Go load failed during prerender: route ID ${event.route.id}, path ${event.url.pathname}, source ${source}: ${answer.failure}`
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
for (const [name, values] of Object.entries(answer.headers ?? {})) {
|
|
145
|
-
event.setHeaders({ [name]: values.join(', ') });
|
|
146
|
-
}
|
|
147
|
-
for (const cookie of answer.cookies ?? []) {
|
|
148
|
-
event.cookies.set(cookie.name, cookie.value, {
|
|
149
|
-
path: cookie.path,
|
|
150
|
-
...(cookie.domain ? { domain: cookie.domain } : {}),
|
|
151
|
-
...(cookie.maxAge ? { maxAge: cookie.maxAge } : {}),
|
|
152
|
-
httpOnly: cookie.httpOnly,
|
|
153
|
-
secure: cookie.secure,
|
|
154
|
-
sameSite: ['lax', 'lax', 'strict', 'none'][cookie.sameSite] ?? 'lax'
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
if (answer.data) {
|
|
158
|
-
const pending = new Map();
|
|
159
|
-
const revivers = {
|
|
160
|
-
...transportDecoders,
|
|
161
|
-
Promise: (id) => {
|
|
162
|
-
let resolve;
|
|
163
|
-
let reject;
|
|
164
|
-
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
165
|
-
pending.set(id, { promise, resolve, reject });
|
|
166
|
-
return promise;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
answer.data = unflatten(answer.data, revivers);
|
|
170
|
-
for (const chunk of answer.chunks ?? []) {
|
|
171
|
-
const deferred = pending.get(chunk.id);
|
|
172
|
-
if (!deferred) throw new Error(`skgo: unknown prerender deferred id ${chunk.id}`);
|
|
173
|
-
if (chunk.error) deferred.reject(new Error(chunk.error));
|
|
174
|
-
else deferred.resolve(unflatten(chunk.data, revivers));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return answer;
|
|
94
|
+
return remoteLoad(module, source, event);
|
|
178
95
|
},
|
|
179
96
|
async skgoPrerenderRemote(module, name, arg, event) {
|
|
180
|
-
|
|
181
|
-
const payload = arg === undefined ? '' : Buffer.from(stringify(arg)).toString('base64url');
|
|
182
|
-
const request = {
|
|
183
|
-
kind: 'remote', module, name, payload, url: event.url.href,
|
|
184
|
-
headers: Object.fromEntries([...event.request.headers].map(([key, value]) => [key, [value]]))
|
|
185
|
-
};
|
|
186
|
-
const raw = await runPrerenderCommand(prerenderBinary, [], prerenderRoot, JSON.stringify(request));
|
|
187
|
-
const answer = JSON.parse(raw);
|
|
188
|
-
return unflatten(JSON.parse(answer.data), transportDecoders)._;
|
|
97
|
+
return remoteFunction(module, name, arg, event);
|
|
189
98
|
}
|
|
190
99
|
})
|
|
191
100
|
};
|
|
@@ -196,8 +105,9 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
196
105
|
// compiles the SSR bundle in is also declared in `vite dev`, where Go
|
|
197
106
|
// pulls one transformed module at a time out of it instead. Each plugin
|
|
198
107
|
// states its own `apply`, so only one of them is ever live.
|
|
199
|
-
vite: { plugins: { post: [environment.plugin, goja.plugin, gojaDevEnvironment(), gojaDevUnchangedFiles({ out })] } },
|
|
108
|
+
vite: { plugins: { pre: [prerenderOwnerPlugin], post: [environment.plugin, goja.plugin, gojaDevEnvironment(), gojaDevUnchangedFiles({ out })] } },
|
|
200
109
|
async adapt(builder) {
|
|
110
|
+
try {
|
|
201
111
|
rmSync(out, { force: true, recursive: true });
|
|
202
112
|
|
|
203
113
|
write(`${out}/env.json`, JSON.stringify(environment.artifact()));
|
|
@@ -332,6 +242,9 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
332
242
|
write(`${out}/.gitkeep`, '');
|
|
333
243
|
|
|
334
244
|
builder.log.minor(`skgo: wrote ${out}/`);
|
|
245
|
+
} finally {
|
|
246
|
+
await prerenderOwner?.cleanup();
|
|
247
|
+
}
|
|
335
248
|
}
|
|
336
249
|
};
|
|
337
250
|
}
|
|
@@ -781,12 +694,6 @@ function checkRemoteIds(clientDir, remotes, hashes) {
|
|
|
781
694
|
}
|
|
782
695
|
}
|
|
783
696
|
|
|
784
|
-
if (called.size === 0 && hashes.size > 0) {
|
|
785
|
-
throw new Error(
|
|
786
|
-
`skgo: kit compiled ${hashes.size} remote module(s) but no remote-function id appears in ${clientDir}. Either nothing imports them, or the ids no longer survive bundling as literals and this check has stopped meaning anything.`
|
|
787
|
-
);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
697
|
const undeclared = [...called].filter(([id]) => !declared.has(id));
|
|
791
698
|
if (undeclared.length) {
|
|
792
699
|
throw new Error(
|