@skgo/sveltekit-adapter 0.16.3 → 0.17.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 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.16.3",
3
+ "version": "0.17.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"
@@ -23,7 +23,8 @@
23
23
  // below shadows the star, which is how the four substitutions take effect.
24
24
  import * as real from 'skgo:kit/remote';
25
25
  import { stringify_remote_arg } from 'skgo:kit/shared';
26
- import { HttpError, Redirect } from '@sveltejs/kit/internal/server';
26
+ import { HandledHttpError, HttpError, Redirect } from '@sveltejs/kit/internal';
27
+ import { DEV } from 'esm-env';
27
28
  import { parse } from 'skgo:kit/transport';
28
29
 
29
30
  export * from 'skgo:kit/app-server';
@@ -47,6 +48,9 @@ export * from 'skgo:kit/app-server';
47
48
  async function host(id, payload) {
48
49
  const raw = await globalThis.__skgo_remote(id, payload);
49
50
  const res = JSON.parse(raw);
51
+ if (res.h) {
52
+ throw new HandledHttpError(res.h);
53
+ }
50
54
  if (res.r) {
51
55
  throw new Redirect(res.r.status, res.r.location);
52
56
  }
@@ -64,9 +68,13 @@ export function query(validate_or_fn, maybe_fn) {
64
68
 
65
69
  export function prerender(validate_or_fn, maybe_fn, options) {
66
70
  const fn = (arg) => host(wrapper.__.id, stringify_remote_arg(arg));
67
- const wrapper = typeof maybe_fn === 'function'
68
- ? real.prerender(validate_or_fn, fn, options)
69
- : real.prerender(fn, maybe_fn);
71
+ const hasValidator = typeof maybe_fn === 'function';
72
+ const normalizedOptions = options ?? (hasValidator ? undefined : maybe_fn);
73
+ const wrapper = !DEV && !normalizedOptions?.dynamic
74
+ ? real.prerender('unchecked', fn, normalizedOptions)
75
+ : hasValidator
76
+ ? real.prerender(validate_or_fn, fn, options)
77
+ : real.prerender(fn, maybe_fn);
70
78
  return wrapper;
71
79
  }
72
80
 
@@ -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();
@@ -20,6 +20,7 @@ import {
20
20
  SvelteKitError,
21
21
  ValidationError
22
22
  } from '@sveltejs/kit/internal/server';
23
+ import { HandledHttpError } from '@sveltejs/kit/internal';
23
24
  import * as devalue from 'skgo:devalue';
24
25
  import { decoders, init_transport, parse } from 'skgo:kit/transport';
25
26
  import { components, universalLoads } from 'skgo:nodes';
@@ -47,6 +48,7 @@ init_transport(transport ?? {});
47
48
  * skgo has no handleError hook, so there is nothing here to await or merge.
48
49
  */
49
50
  function handle_error(error) {
51
+ if (error instanceof HandledHttpError) return error.body;
50
52
  if (error instanceof HttpError) return error.body;
51
53
  if (error instanceof SvelteKitError) return { status: error.status, message: error.text };
52
54
  if (error instanceof ValidationError) return { status: 400, message: 'Bad Request' };
@@ -418,6 +420,8 @@ globalThis.__skgo_render = function (req_json) {
418
420
  (err) => {
419
421
  if (err instanceof Redirect) {
420
422
  result.redirect = { status: err.status, location: err.location };
423
+ } else if (err instanceof HandledHttpError) {
424
+ result.handled_error = err.body;
421
425
  } else {
422
426
  result.failure = (err && (err.stack || err.message)) || String(err);
423
427
  }
@@ -427,6 +431,8 @@ globalThis.__skgo_render = function (req_json) {
427
431
  } catch (err) {
428
432
  if (err instanceof Redirect) {
429
433
  result.redirect = { status: err.status, location: err.location };
434
+ } else if (err instanceof HandledHttpError) {
435
+ result.handled_error = err.body;
430
436
  } else {
431
437
  result.failure = (err && (err.stack || err.message)) || String(err);
432
438
  }
@@ -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,2 @@
1
+ /** Internal bridge used by generated declared-prerender input modules. */
2
+ export function remoteInputs<Input = unknown>(module: string, name: string): Promise<Input[]>;
@@ -0,0 +1,824 @@
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
+ const request = {
756
+ kind: 'load', module, url: event.url.href, routeId: event.route.id,
757
+ params: Object.fromEntries(Object.entries(event.params)), parent: await event.parent(), headers
758
+ };
759
+ const raw = await invokeWorker(request);
760
+ const answer = JSON.parse(raw);
761
+ if (typeof answer.failure === 'string') {
762
+ throw new Error(`skgo: Go load failed during prerender: route ID ${event.route.id}, path ${event.url.pathname}, source ${source}: ${answer.failure}`);
763
+ }
764
+ for (const [name, values] of Object.entries(answer.headers ?? {})) event.setHeaders({ [name]: values.join(', ') });
765
+ for (const cookie of answer.cookies ?? []) {
766
+ event.cookies.set(cookie.name, cookie.value, {
767
+ path: cookie.path, ...(cookie.domain ? { domain: cookie.domain } : {}),
768
+ ...(cookie.maxAge ? { maxAge: cookie.maxAge } : {}), httpOnly: cookie.httpOnly,
769
+ secure: cookie.secure, sameSite: ['lax', 'lax', 'strict', 'none'][cookie.sameSite] ?? 'lax'
770
+ });
771
+ }
772
+ if (answer.data) {
773
+ const pending = new Map();
774
+ const revivers = {
775
+ ...transport.decoders,
776
+ Promise: (id) => {
777
+ let resolve;
778
+ let reject;
779
+ const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
780
+ pending.set(id, { promise, resolve, reject });
781
+ return promise;
782
+ }
783
+ };
784
+ try {
785
+ answer.data = devalue.unflatten(answer.data, revivers);
786
+ for (const chunk of answer.chunks ?? []) {
787
+ const deferred = pending.get(chunk.id);
788
+ if (!deferred) throw makeError(`skgo: unknown prerender deferred id ${chunk.id}`);
789
+ if (chunk.error) deferred.reject(new Error(chunk.error));
790
+ else deferred.resolve(devalue.unflatten(chunk.data, revivers));
791
+ }
792
+ } catch (error) {
793
+ return reportWorkerFatal(error);
794
+ }
795
+ }
796
+ return answer;
797
+ }
798
+
799
+ /** The emulator callback for a Go prerender remote, executing in Kit's worker. */
800
+ export async function remoteFunction(module, name, arg, event) {
801
+ let kit;
802
+ try { kit = await runtime(); } catch (error) { return reportWorkerFatal(error); }
803
+ const { internal, transport } = kit;
804
+ let payload;
805
+ try { payload = arg === undefined ? '' : Buffer.from(transport.stringify(arg), 'utf8').toString('base64url'); }
806
+ catch (error) { return reportWorkerFatal(error); }
807
+ const headers = Object.fromEntries([...event.request.headers].map(([key, value]) => [key, [value]]));
808
+ const raw = await invokeWorker({
809
+ kind: 'remote', module, name, payload, url: event.url.href, headers
810
+ });
811
+ const answer = JSON.parse(raw);
812
+ if (answer.type === 'error' && answer.kind === 'app') throw new internal.HttpError(answer.error);
813
+ if (answer.type === 'error' && answer.kind === 'unknown') throw new Error(answer.diagnostic);
814
+ if (answer.type === 'redirect') throw new internal.Redirect(answer.redirect.status, answer.redirect.location);
815
+ if (answer.type !== 'result' || typeof answer.data !== 'string') throw makeError('skgo prerender remote response is malformed');
816
+ try {
817
+ const result = transport.parse(answer.data);
818
+ if (!result || typeof result !== 'object' || !Object.hasOwn(result, '_')) {
819
+ throw makeError('skgo prerender remote result omitted its value member');
820
+ }
821
+ return result._;
822
+ }
823
+ catch (error) { return reportWorkerFatal(error); }
824
+ }
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 prerenderBinary;
87
- let prerenderRoot;
88
- let prerenderBuild;
89
- let unflatten;
90
- let stringify;
91
- let transportDecoders = {};
92
- async function buildPrerenderBinary() {
93
- if (!prerenderBuild) {
94
- prerenderBuild = (async () => {
95
- const generated = readGenerated();
96
- if (!generated.prerender) throw new Error('skgo: generated prerender command is missing');
97
- prerenderRoot = resolve(process.cwd(), generated.prerender.root);
98
- const dir = mkdtempSync(join(tmpdir(), 'skgo-prerender-'));
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
- await prerenderBuild;
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
- await buildPrerenderBinary();
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
- await buildPrerenderBinary();
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(