@skgo/sveltekit-adapter 0.19.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -7,6 +7,10 @@ interface AdapterOptions {
7
7
  * @default 'build'
8
8
  */
9
9
  out?: string;
10
+ /** Authored Go build-service package, relative to the app's Go module root.
11
+ * Use RunPrerenderService to share renderer defaults with production SSR.
12
+ * Defaults to the generated prerender entry. */
13
+ prerenderPackage?: string;
10
14
  /**
11
15
  * Whether to write gzip and brotli copies of the assets and the prerendered
12
16
  * pages beside them. The Go server serves whichever the browser asked for.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skgo/sveltekit-adapter",
3
- "version": "0.19.0",
3
+ "version": "0.22.1",
4
4
  "description": "SvelteKit adapter for skgo: builds a frontend that a Go binary renders and answers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -44,6 +44,7 @@
44
44
  "skgo-adapter/identity.js",
45
45
  "skgo-adapter/polyfill.js",
46
46
  "skgo-adapter/prerender-csp.js",
47
+ "skgo-adapter/prerender-modules.js",
47
48
  "skgo-adapter/prerender.js",
48
49
  "skgo-adapter/prerender.d.ts"
49
50
  ],
@@ -60,12 +60,12 @@ function handle_error(error) {
60
60
  * kit's remote wrappers read during a render are populated. is_in_render is
61
61
  * what makes kit refuse a command.
62
62
  */
63
- function make_state() {
63
+ function make_state(req) {
64
64
  return {
65
- getClientAddress: () => '127.0.0.1',
65
+ getClientAddress: () => __skgo_client_address(),
66
66
  error: false,
67
67
  rerouted_url: null,
68
- depth: 0,
68
+ depth: req.is_sub_request ? 1 : 0,
69
69
  remote: {
70
70
  data: null,
71
71
  implicit: null,
@@ -171,7 +171,7 @@ function make_event(req, url, serialized) {
171
171
  serialize: () => ''
172
172
  },
173
173
  fetch: create_fetch(req, url, serialized),
174
- getClientAddress: () => req.client_address ?? '127.0.0.1',
174
+ getClientAddress: () => __skgo_client_address(),
175
175
  locals: {},
176
176
  params: req.params ?? {},
177
177
  platform: undefined,
@@ -179,9 +179,9 @@ function make_event(req, url, serialized) {
179
179
  route: { id: req.route_id ?? null },
180
180
  setHeaders: () => {},
181
181
  url,
182
- isDataRequest: false,
183
- isSubRequest: false,
184
- isRemoteRequest: false,
182
+ isDataRequest: !!req.is_data_request,
183
+ isSubRequest: !!req.is_sub_request,
184
+ isRemoteRequest: !!req.is_remote_request,
185
185
  tracing: { enabled: false }
186
186
  };
187
187
  }
@@ -362,7 +362,7 @@ globalThis.__skgo_render = function (req_json) {
362
362
  try {
363
363
  const req = JSON.parse(req_json);
364
364
  const url = new URL(req.url);
365
- const state = make_state();
365
+ const state = make_state(req);
366
366
  const serialized = new Set();
367
367
  const event = make_event(req, url, serialized);
368
368
  const fetched = [];
@@ -338,6 +338,8 @@ export function gojaEnvironment() {
338
338
  load: {
339
339
  order: 'pre',
340
340
  handler(id) {
341
+ // Go owns handle at runtime; this build-only hook has no engine code.
342
+ if (/\/src\/hooks\.server\.(?:ts|js)(?:\?|$)/.test(id)) return 'export {};';
341
343
  if (/\/\.svelte-kit\/skgo-env-(?:values\.js|runtime\.json)(?:\?|$)/.test(id)) {
342
344
  throw new Error('skgo: environment build inputs are private');
343
345
  }
@@ -978,6 +980,8 @@ export function gojaDevEnvironment({ outDir = '.svelte-kit' } = {}) {
978
980
  load: {
979
981
  order: 'pre',
980
982
  handler(id) {
983
+ // Go owns handle at runtime; this build-only hook has no engine code.
984
+ if (/\/src\/hooks\.server\.(?:ts|js)(?:\?|$)/.test(id)) return 'export {};';
981
985
  if (/\/\.svelte-kit\/skgo-env-(?:values\.js|runtime\.json)(?:\?|$)/.test(id)) {
982
986
  throw new Error('skgo: environment build inputs are private');
983
987
  }
@@ -64,7 +64,7 @@ export function validateGenerated(parsed) {
64
64
  );
65
65
  }
66
66
  }
67
- return { remotes: parsed.remotes, loads: parsed.loads, actions: parsed.actions, endpoints: parsed.endpoints, prerender: parsed.prerender };
67
+ return { remotes: parsed.remotes, loads: parsed.loads, actions: parsed.actions, endpoints: parsed.endpoints, prerender: parsed.prerender, build: parsed.build };
68
68
  }
69
69
 
70
70
  /**
@@ -0,0 +1,134 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { join, relative } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { validateGenerated } from './generated.js';
6
+
7
+ // Use the parser shipped with the application's Vite, as the goja environment
8
+ // does. Parsing the original TS/JS declarations preserves Kit's native wrappers,
9
+ // actions, types and module identities; only build callback bodies change.
10
+ const app = createRequire(join(process.cwd(), 'package.json'));
11
+ const { parseSync } = await import(pathToFileURL(app.resolve('vite/rolldown/experimental')).href);
12
+ const header = '// Code generated by skgo. DO NOT EDIT.';
13
+ const helper = '@skgo/sveltekit-adapter/prerender';
14
+
15
+ export function prerenderModules() {
16
+ let root;
17
+ let generated;
18
+ return {
19
+ name: 'skgo-prerender-modules',
20
+ apply: 'build',
21
+ applyToEnvironment: (environment) => environment.name === 'ssr',
22
+ configResolved(config) { root = config.root; },
23
+ buildStart() {
24
+ generated = validateGenerated(JSON.parse(readFileSync(join(root, 'skgo.remotes.json'), 'utf8')));
25
+ if (!generated.build || typeof generated.build !== 'object' || Array.isArray(generated.build)) {
26
+ throw new Error('skgo: missing build declarations; run `go generate ./...`.');
27
+ }
28
+ for (const [module, declaration] of Object.entries(generated.build)) {
29
+ if (!module.startsWith('src/') || module.split('/').some((part) => part === '.' || part === '..') || !/\.(ts|js)$/.test(module) || !declaration || typeof declaration !== 'object' || Array.isArray(declaration)) {
30
+ throw new Error(`skgo: malformed build declaration for ${module}; run \`go generate ./...\`.`);
31
+ }
32
+ const fields = Object.keys(declaration);
33
+ if (fields.length !== 1 || !['source', 'remotes', 'endpoint', 'hook'].includes(fields[0]) ||
34
+ ('source' in declaration && (typeof declaration.source !== 'string' || !declaration.source)) ||
35
+ ('remotes' in declaration && (!Array.isArray(declaration.remotes) || !declaration.remotes.length)) ||
36
+ ('endpoint' in declaration && declaration.endpoint !== true) ||
37
+ ('hook' in declaration && declaration.hook !== true)) {
38
+ throw new Error(`skgo: malformed build declaration for ${module}; run \`go generate ./...\`.`);
39
+ }
40
+ }
41
+ for (const module of generated.loads) {
42
+ if (typeof generated.build[module]?.source !== 'string' || !generated.build[module].source) {
43
+ throw new Error(`skgo: missing Go load source for ${module}; run \`go generate ./...\`.`);
44
+ }
45
+ }
46
+ },
47
+ load(id) {
48
+ const module = relative(root, id).split('\\').join('/');
49
+ if (!/\.(ts|js)$/.test(module)) return;
50
+ const declaration = generated.build[module];
51
+ // Inspect server declarations even without metadata, so a missing
52
+ // entry cannot quietly execute a generated throwing stub at build time.
53
+ if (!declaration && !/(\.remote|\/\+(page|layout)\.server|\/\+server|\/hooks\.server)\.(ts|js)$/.test(module)) return;
54
+ const code = readFileSync(id, 'utf8');
55
+ if (!code.startsWith(header)) {
56
+ if (declaration) throw new Error(`skgo: build declaration for authored module ${module}`);
57
+ return;
58
+ }
59
+ const parsed = parseSync(id, code);
60
+ if (parsed.errors.length) throw new Error(`skgo: cannot parse generated module ${module}`);
61
+ const exports = new Map();
62
+ for (const node of parsed.program.body) {
63
+ if (node.type !== 'ExportNamedDeclaration' || node.exportKind === 'type') continue;
64
+ for (const item of node.declaration?.declarations ?? []) exports.set(item.id.name, item.init);
65
+ }
66
+ const fail = () => { throw new Error(`skgo: build declarations disagree with ${module}; run \`go generate ./...\`.`); };
67
+ const edits = [];
68
+ let imports = '';
69
+ if (exports.has('load')) {
70
+ if (!declaration?.source || !generated.loads.includes(module)) fail();
71
+ const fn = exports.get('load');
72
+ if (fn?.type !== 'ArrowFunctionExpression' || fn.params.length !== 1) fail();
73
+ const event = fn.params[0].name;
74
+ imports += "import { error as $skgoError, redirect as $skgoRedirect } from '@sveltejs/kit';\n";
75
+ edits.push([fn.body.start, fn.body.end, `{
76
+ const bridge = ${event}.platform?.skgoPrerenderLoad;
77
+ if (!bridge) throw new Error('skgo: prerender Go load bridge unavailable');
78
+ const answer = await bridge(${JSON.stringify(module)}, ${JSON.stringify(declaration.source)}, ${event});
79
+ if (answer.redirect) $skgoRedirect(answer.redirect.status, answer.redirect.location);
80
+ if (answer.error) $skgoError(answer.error.status, answer.error.message);
81
+ if (answer.data === undefined) throw new Error('skgo: prerender Go load returned no data');
82
+ return answer.data;
83
+ }`]);
84
+ } else if (declaration?.source) fail();
85
+ if (/\/\+server\.(ts|js)$/.test(module)) {
86
+ if (declaration?.endpoint !== true) fail();
87
+ for (const fn of exports.values()) {
88
+ if (!fn) fail();
89
+ edits.push([fn.start, fn.end, `(event) => {
90
+ const bridge = event.platform?.skgoPrerenderEndpoint;
91
+ if (!bridge) throw new Error('skgo: prerender Go endpoint bridge unavailable');
92
+ return bridge(event);
93
+ }`]);
94
+ }
95
+ }
96
+ const remotes = declaration?.remotes ?? [];
97
+ if (!Array.isArray(remotes)) fail();
98
+ const prerenders = [...exports].filter(([, fn]) => fn?.type === 'CallExpression' && fn.callee.name === 'prerender');
99
+ if (prerenders.length !== remotes.length) fail();
100
+ if (remotes.length) imports += "import { getRequestEvent as $skgoEvent } from '$app/server';\n";
101
+ if (remotes.some((fn) => fn.inputs)) imports += `import { remoteInputs as $skgoInputs } from '${helper}';\n`;
102
+ const seen = new Set();
103
+ for (const remote of remotes) {
104
+ const call = exports.get(remote.name);
105
+ if (seen.has(remote.name) || typeof remote.argument !== 'boolean' || typeof remote.inputs !== 'boolean' || call?.callee.name !== 'prerender') fail();
106
+ seen.add(remote.name);
107
+ const callback = call.arguments[remote.argument ? 1 : 0];
108
+ if (call.arguments.length !== (remote.argument ? 2 : 1) || callback?.type !== 'ArrowFunctionExpression' || callback.params.length !== (remote.argument ? 1 : 0)) fail();
109
+ const arg = remote.argument ? callback.params[0].name : 'undefined';
110
+ edits.push([callback.body.start, callback.body.end, `{
111
+ const event = $skgoEvent();
112
+ const bridge = event.platform?.skgoPrerenderRemote;
113
+ if (!bridge) throw new Error('skgo: prerender Go remote bridge unavailable');
114
+ return bridge(${JSON.stringify(module)}, ${JSON.stringify(remote.name)}, ${arg}, event);
115
+ }`]);
116
+ if (remote.inputs) edits.push([callback.end, callback.end, `, { inputs: () => $skgoInputs(${JSON.stringify(module)}, ${JSON.stringify(remote.name)}) }`]);
117
+ }
118
+ if (/\/hooks\.server\.(ts|js)$/.test(module)) {
119
+ if (declaration?.hook !== true || exports.has('handle') || exports.has('handleFetch')) fail();
120
+ return `import { requestHandle, requestFetch } from '${helper}';
121
+ ${code}
122
+ export const handle = ({ event, resolve }) => requestHandle(event, resolve);
123
+ export const handleFetch = (input) => requestFetch(input);
124
+ `;
125
+ }
126
+ if (!edits.length) return;
127
+ let result = code;
128
+ for (const [start, end, replacement] of edits.sort((a, b) => b[0] - a[0])) {
129
+ result = result.slice(0, start) + replacement + result.slice(end);
130
+ }
131
+ return imports + result;
132
+ }
133
+ };
134
+ }
@@ -1,2 +1,6 @@
1
- /** Internal bridge used by generated declared-prerender input modules. */
1
+ /** Internal bridge used by adapter build modules. */
2
2
  export function remoteInputs<Input = unknown>(module: string, name: string): Promise<Input[]>;
3
+
4
+ /** Build-only logical request lifecycle used by adapter build hooks. */
5
+ export function requestHandle(event: import('@sveltejs/kit').RequestEvent, resolve: (event: import('@sveltejs/kit').RequestEvent, options?: import('@sveltejs/kit/hooks').ResolveOptions) => Promise<Response>): Promise<Response>;
6
+ export function requestFetch(input: Parameters<import('@sveltejs/kit/hooks').HandleFetch>[0]): Promise<Response>;
@@ -7,15 +7,19 @@ import process from "node:process";
7
7
  import { tmpdir } from "node:os";
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
- import { isMainThread } from "node:worker_threads";
10
+ import { isMainThread, BroadcastChannel, receiveMessageOnPort } from "node:worker_threads";
11
+ import { AsyncLocalStorage } from "node:async_hooks";
11
12
 
12
13
  const owners = new Map();
13
14
  const SERVICE_URL = "SKGO_PRERENDER_SERVICE_URL";
14
15
  const SERVICE_SECRET = "SKGO_PRERENDER_SERVICE_SECRET";
16
+ const PREDICATE_CHANNEL = "SKGO_PRERENDER_PREDICATE_CHANNEL";
15
17
  const CALLBACK_TIMEOUT = 30_000;
16
18
  const STARTUP_TIMEOUT = 60_000;
17
19
  const SHUTDOWN_GRACE = 150;
18
20
  const CLEANUP_TIMEOUT = 5_000;
21
+ const internalFetch = new AsyncLocalStorage();
22
+ const cookieRelays = new WeakMap();
19
23
  function appRoot() {
20
24
  return resolve(process.cwd());
21
25
  }
@@ -98,6 +102,7 @@ function validateEnvelope(request, raw) {
98
102
  }
99
103
  if (!value || typeof value !== "object" || Array.isArray(value))
100
104
  throw new Error("skgo prerender command returned a non-object response");
105
+ if (["begin", "response", "end", "endpoint", "cookies", "resolve-option"].includes(request.kind)) return;
101
106
  if (request.kind === "remote-inputs") {
102
107
  if (typeof value.inputs !== "string")
103
108
  throw new Error("skgo prerender inputs response has no devalue payload");
@@ -318,7 +323,7 @@ function startJob(owner, command, args, options = {}) {
318
323
  }
319
324
 
320
325
  /** The main build thread owns its compiler, HTTP service and scratch directory. */
321
- export function createPrerenderOwner(root = appRoot()) {
326
+ export function createPrerenderOwner(root = appRoot(), prerenderPackage) {
322
327
  if (!isMainThread) return null;
323
328
  root = resolve(root);
324
329
  if (owners.has(root)) return owners.get(root);
@@ -328,7 +333,7 @@ export function createPrerenderOwner(root = appRoot()) {
328
333
  const owner = {
329
334
  root,
330
335
  cwd: resolve(root, manifest.prerender.root),
331
- pkg: manifest.prerender.package,
336
+ pkg: prerenderPackage ?? manifest.prerender.package,
332
337
  jobs: new Set(),
333
338
  closing: false,
334
339
  failed: null,
@@ -336,10 +341,17 @@ export function createPrerenderOwner(root = appRoot()) {
336
341
  service: null,
337
342
  previousEnv: null,
338
343
  signals: new Map(),
344
+ pendingPredicates: new Map(),
339
345
  cleanupPromise: null,
340
346
  preparePromise: null,
341
347
  fail(error) {
342
348
  owner.failed ??= error;
349
+ for (const [id, cell] of owner.pendingPredicates) {
350
+ owner.channel?.postMessage({id, error: owner.failed.stack ?? owner.failed.message ?? String(owner.failed)});
351
+ Atomics.store(cell, 0, 1);
352
+ Atomics.notify(cell, 0);
353
+ }
354
+ owner.pendingPredicates.clear();
343
355
  return owner.cleanup().catch((cleanupError) => {
344
356
  owner.cleanupFailure = cleanupError;
345
357
  if (typeof owner.failed === "object" && owner.failed !== cleanupError) {
@@ -448,18 +460,48 @@ export function createPrerenderOwner(root = appRoot()) {
448
460
  publish() {
449
461
  if (owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
450
462
  if (owner.previousEnv) return;
463
+ owner.channel = new BroadcastChannel(`skgo-prerender-${randomUUID()}`);
464
+ owner.channel.unref();
465
+ owner.channel.onmessage = async ({ data }) => {
466
+ if (data?.type === "failure") {
467
+ void owner.fail(new Error(data.error)).catch(reportOwnerFailure);
468
+ return;
469
+ }
470
+ if (data?.type !== "predicate") return;
471
+ const { id, cell, request } = data;
472
+ owner.pendingPredicates.set(id, cell);
473
+ let reply;
474
+ try {
475
+ if (owner.failed || owner.closing) throw owner.failed ?? new Error("skgo prerender owner closed");
476
+ const answer = JSON.parse(await invokeService({ ...request, kind: "resolve-option" }));
477
+ if (typeof answer.value !== "boolean") throw new Error("skgo prerender predicate returned no boolean");
478
+ reply = { id, value: answer.value };
479
+ } catch (error) {
480
+ reply = { id, error: error.stack ?? error.message ?? String(error) };
481
+ }
482
+ if (!owner.pendingPredicates.delete(id)) return;
483
+ // Wake before fail() can wait for the worker/service to stop.
484
+ owner.channel?.postMessage(reply);
485
+ Atomics.store(cell, 0, 1);
486
+ Atomics.notify(cell, 0);
487
+ if (reply.error) void owner.fail(new Error(reply.error)).catch(reportOwnerFailure);
488
+ };
451
489
  owner.previousEnv = new Map([
452
490
  [SERVICE_URL, process.env[SERVICE_URL]],
453
491
  [SERVICE_SECRET, process.env[SERVICE_SECRET]],
492
+ [PREDICATE_CHANNEL, process.env[PREDICATE_CHANNEL]],
454
493
  ]);
455
494
  process.env[SERVICE_URL] = owner.connection.url;
456
495
  process.env[SERVICE_SECRET] = owner.connection.secret;
496
+ process.env[PREDICATE_CHANNEL] = owner.channel.name;
457
497
  },
458
498
  cleanup() {
459
499
  if (owner.cleanupPromise) return owner.cleanupPromise;
460
500
  owner.closing = true;
461
501
  owner.cleanupPromise = (async () => {
462
502
  const errors = [];
503
+ owner.channel?.close();
504
+ owner.channel = null;
463
505
  for (const job of owner.jobs) {
464
506
  try {
465
507
  await stopJob(job, job === owner.service);
@@ -562,10 +604,10 @@ export function installPrerenderFailureBoundary(config, owner) {
562
604
  async function invokeService(request) {
563
605
  const url = process.env[SERVICE_URL];
564
606
  const secret = process.env[SERVICE_SECRET];
565
- const identity = `${request.kind} ${request.module}/${request.name ?? ""}`;
607
+ const identity = request.kind === "resolve-option" ? `${request.option} resolve callback` : `${request.kind} ${request.module}/${request.name ?? ""}`;
566
608
  if (!url || !secret) throw new Error(`skgo prerender service unavailable for ${identity}`);
567
609
  const { kind, ...body } = request;
568
- const path = { load: "/load", remote: "/remote", "remote-inputs": "/inputs" }[kind];
610
+ const path = { load: "/load", remote: "/remote", "remote-inputs": "/inputs", begin: "/begin", response: "/response", end: "/end", endpoint: "/endpoint", cookies: "/cookies", "resolve-option": "/resolve-option" }[kind];
569
611
  const controller = new AbortController();
570
612
  const timer = setTimeout(() => controller.abort(), CALLBACK_TIMEOUT);
571
613
  try {
@@ -657,6 +699,7 @@ export async function remoteLoad(module, source, event) {
657
699
  const request = {
658
700
  kind: "load",
659
701
  module,
702
+ handle: event.platform?.skgoRequestHandle,
660
703
  url: event.url.href,
661
704
  routeId: event.route.id,
662
705
  params: {},
@@ -677,18 +720,7 @@ export async function remoteLoad(module, source, event) {
677
720
  `skgo: Go load failed during prerender: route ID ${event.route.id}, path ${event.url.pathname}, source ${source}: ${answer.failure}`,
678
721
  );
679
722
  }
680
- for (const [name, values] of Object.entries(answer.headers ?? {}))
681
- event.setHeaders({ [name]: values.join(", ") });
682
- for (const cookie of answer.cookies ?? []) {
683
- event.cookies.set(cookie.name, cookie.value, {
684
- path: cookie.path,
685
- ...(cookie.domain ? { domain: cookie.domain } : {}),
686
- ...(cookie.maxAge ? { maxAge: cookie.maxAge } : {}),
687
- httpOnly: cookie.httpOnly,
688
- secure: cookie.secure,
689
- sameSite: ["lax", "lax", "strict", "none"][cookie.sameSite] ?? "lax",
690
- });
691
- }
723
+ applyRequestEffects(event, answer);
692
724
  if (answer.data) {
693
725
  const pending = new Map();
694
726
  const revivers = {
@@ -728,6 +760,7 @@ export async function remoteFunction(module, name, arg, event) {
728
760
  kind: "remote",
729
761
  module,
730
762
  name,
763
+ handle: event.platform?.skgoRequestHandle,
731
764
  payload,
732
765
  url: event.url.href,
733
766
  headers,
@@ -745,3 +778,176 @@ export async function remoteFunction(module, name, arg, event) {
745
778
  }
746
779
  return result._;
747
780
  }
781
+
782
+
783
+ function applyRequestEffects(event, answer) {
784
+ for (const [name, values] of Object.entries(answer.headers ?? {}))
785
+ event.setHeaders({ [name]: values.join(", ") });
786
+ for (const cookie of answer.cookies ?? [])
787
+ event.cookies.set(cookie.name, cookie.value, {
788
+ path: cookie.path, ...(cookie.domain ? { domain: cookie.domain } : {}),
789
+ ...(cookie.maxAge !== undefined ? { maxAge: cookie.maxAge } : {}), httpOnly: cookie.httpOnly,
790
+ ...(cookie.expires ? { expires: new Date(cookie.expires) } : {}),
791
+ ...(cookie.partitioned ? { partitioned: true } : {}),
792
+ ...(cookie.priority ? { priority: cookie.priority } : {}),
793
+ ...(cookie.raw ? { encode: (value) => value } : {}),
794
+ secure: cookie.secure, sameSite: [false, false, "lax", "strict", "none"][cookie.sameSite] ?? false,
795
+ });
796
+ }
797
+
798
+ /** Generated hooks.server calls this only while Kit is building. */
799
+ export async function requestHandle(event, resolveRequest) {
800
+ // handleFetch's supplied fetch reaches this hook only for Kit subrequests.
801
+ const fetching = internalFetch.getStore();
802
+ if (fetching) fetching.internal = true;
803
+ const { manifest } = await import(pathToFileURL(join(appRoot(), ".svelte-kit/output/server/manifest-full.js")).href);
804
+ const prefix = manifest.app_path.slice(0, -manifest.app_dir.length);
805
+ const base = prefix ? "/" + prefix.slice(0, -1) : "";
806
+ const routes = await Promise.all(manifest.routes.map(async (route) => ({
807
+ id: route.id, pattern: route.pattern.source, params: route.params,
808
+ ...(route.page ? { page: route.page } : {}),
809
+ ...(route.endpoint ? { endpoint: { methods: Object.keys(await route.endpoint()).filter((key) => ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "QUERY", "fallback"].includes(key)).map((key) => key === "fallback" ? "*" : key) } } : {}),
810
+ })));
811
+ const route = routes.find((candidate) => candidate.id === event.route.id);
812
+ const { decode_pathname } = await import(pathToFileURL(join(kitRootFor(appRoot()), "src/utils/url.js")).href);
813
+ const answer = JSON.parse(await invokeService({
814
+ kind: "begin", method: event.request.method, url: event.request.url, logicalUrl: event.url.href,
815
+ headers: Object.fromEntries([...event.request.headers].map(([key, value]) => [key, [value]])),
816
+ body: event.request.method === "GET" || event.request.method === "HEAD" ? "" : Buffer.from(await event.request.clone().arrayBuffer()).toString("base64"),
817
+ routeId: event.route.id ?? "", routePattern: route?.pattern ?? "", routeParams: route?.params ?? [],
818
+ routePath: decode_pathname(event.url.pathname).slice(base.length) || "/",
819
+ isSubRequest: event.isSubRequest,
820
+ manifest: { appDir: manifest.app_dir, base, routes },
821
+ }));
822
+ const handle = answer.handle;
823
+ if (!handle) throw new Error("skgo prerender begin omitted request handle");
824
+ let ended = false;
825
+ const end = async () => {
826
+ if (ended) return;
827
+ ended = true;
828
+ await invokeService({ kind: "end", handle });
829
+ };
830
+ try {
831
+ applyRequestEffects(event, answer);
832
+ const { internal } = await runtime();
833
+ if (answer.redirect) throw new internal.Redirect(answer.redirect.status, answer.redirect.location);
834
+ if (answer.error) throw new internal.HttpError(answer.error);
835
+ let final = answer.response;
836
+ if (answer.resolve) {
837
+ event.platform ??= {};
838
+ event.platform.skgoRequestHandle = handle;
839
+ const options = {};
840
+ if (answer.options?.transform) options.transformPageChunk = async ({ html, done }) => {
841
+ try {
842
+ const result = JSON.parse(await invokeService({ kind: "resolve-option", option: "transform", handle, html, done }));
843
+ if (typeof result.html !== "string") throw new Error("skgo prerender transform returned no HTML");
844
+ return result.html;
845
+ } catch (error) { failPredicateRelay(error); }
846
+ };
847
+ if (answer.options?.filter) options.filterSerializedResponseHeaders = (name, value) => predicate(handle, "filter", { name, value });
848
+ if (answer.options?.preload) options.preload = (input) => predicate(handle, "preload", { input });
849
+ const response = await resolveRequest(event, options);
850
+ const headers = {};
851
+ for (const [name, value] of response.headers) headers[name] = [value];
852
+ if (response.headers.getSetCookie) headers["set-cookie"] = response.headers.getSetCookie();
853
+ const result = JSON.parse(await invokeService({kind: "response", handle, response: {
854
+ status: response.status, headers,
855
+ body: Buffer.from(await response.arrayBuffer()).toString("base64"),
856
+ }}));
857
+ if (result.redirect) throw new internal.Redirect(result.redirect.status, result.redirect.location);
858
+ if (result.error) throw new internal.HttpError(result.error);
859
+ final = result.response;
860
+ }
861
+ if (!final || !Number.isInteger(final.status)) throw new Error("skgo prerender request omitted final response");
862
+ const headers = new Headers();
863
+ for (const [name, values] of Object.entries(final.headers ?? {}))
864
+ for (const value of values ?? []) headers.append(name, value);
865
+ if (event.request.method === "HEAD" || [204, 205, 304].includes(final.status)) {
866
+ await end();
867
+ return new Response(null, { status: final.status, headers });
868
+ }
869
+ const bytes = Buffer.from(final.body ?? "", "base64");
870
+ let emitted = false;
871
+ const body = new ReadableStream({
872
+ async pull(controller) {
873
+ try {
874
+ if (!emitted) { emitted = true; controller.enqueue(bytes); }
875
+ else { await end(); controller.close(); }
876
+ } catch (error) { controller.error(error); }
877
+ },
878
+ cancel: end,
879
+ }, { highWaterMark: 0 });
880
+ return new Response(body, { status: final.status, headers });
881
+ } catch (error) {
882
+ await end();
883
+ throw error;
884
+ }
885
+ }
886
+
887
+ /** Kit has applied internal response cookies before its supplied fetch returns. */
888
+ export async function requestFetch({ event, request, fetch }) {
889
+ const scope = { internal: false };
890
+ return internalFetch.run(scope, async () => {
891
+ const response = await fetch(request);
892
+ const cookies = response.headers.getSetCookie();
893
+ const handle = event.platform?.skgoRequestHandle;
894
+ if (scope.internal && handle && cookies.length) {
895
+ // Match the order in which Kit applied concurrent fetch responses.
896
+ const previous = cookieRelays.get(event.platform) ?? Promise.resolve();
897
+ const relay = previous.then(() => invokeService({ kind: "cookies", handle, url: request.url, cookies }));
898
+ cookieRelays.set(event.platform, relay);
899
+ await relay;
900
+ }
901
+ return response;
902
+ });
903
+ }
904
+
905
+ /** A generated server-route stub forwards only its build-time execution. */
906
+ export async function remoteEndpoint(event) {
907
+ const answer = JSON.parse(await invokeService({kind: "endpoint", handle:event.platform?.skgoRequestHandle}));
908
+ applyRequestEffects(event,answer);
909
+ const response = answer.response;
910
+ if (!response || !Number.isInteger(response.status)) throw new Error("skgo prerender endpoint omitted response");
911
+ const headers = new Headers();
912
+ for (const [name,values] of Object.entries(response.headers ?? {}))
913
+ for (const value of values ?? []) headers.append(name,value);
914
+ const body = event.request.method === "HEAD" || [204,205,304].includes(response.status) ? null : Buffer.from(response.body ?? "","base64");
915
+ return new Response(body,{status:response.status,headers});
916
+ }
917
+
918
+ // The single Kit prerender worker executes predicates synchronously. Replies
919
+ // carry the result; the cell is only the bounded completion notification.
920
+ let predicateChannel;
921
+ let predicateFailure;
922
+ let predicateID = 0;
923
+ const predicateCell = new Int32Array(new SharedArrayBuffer(4));
924
+ function relayChannel() {
925
+ if (!predicateChannel) {
926
+ const name = process.env[PREDICATE_CHANNEL];
927
+ if (!name) throw new Error("skgo prerender predicate owner unavailable");
928
+ predicateChannel = new BroadcastChannel(name);
929
+ predicateChannel.unref();
930
+ }
931
+ return predicateChannel;
932
+ }
933
+ function failPredicateRelay(error) {
934
+ predicateFailure ??= error instanceof Error ? error : new Error(String(error));
935
+ try { relayChannel().postMessage({ type: "failure", error: predicateFailure.stack ?? predicateFailure.message }); } catch {}
936
+ throw predicateFailure;
937
+ }
938
+ function predicate(handle, option, args) {
939
+ if (predicateFailure) throw predicateFailure;
940
+ try {
941
+ const channel = relayChannel();
942
+ const id = ++predicateID;
943
+ Atomics.store(predicateCell, 0, 0);
944
+ channel.postMessage({ type: "predicate", id, cell: predicateCell, request: { handle, option, ...args } });
945
+ const status = Atomics.wait(predicateCell, 0, 0, CALLBACK_TIMEOUT);
946
+ if (status === "timed-out") throw new Error(`skgo prerender ${option} predicate timed out after ${CALLBACK_TIMEOUT}ms`);
947
+ const reply = receiveMessageOnPort(channel)?.message;
948
+ if (!reply || reply.id !== id || (typeof reply.value === "boolean") === (typeof reply.error === "string"))
949
+ throw new Error("skgo prerender predicate reply missing or malformed");
950
+ if (reply.error !== undefined) throw new Error(reply.error);
951
+ return reply.value;
952
+ } catch (error) { return failPredicateRelay(error); }
953
+ }
package/skgo-adapter.js CHANGED
@@ -18,9 +18,11 @@ import {
18
18
  createPrerenderOwner,
19
19
  installPrerenderFailureBoundary,
20
20
  remoteFunction,
21
+ remoteEndpoint,
21
22
  remoteLoad
22
23
  } from './skgo-adapter/prerender.js';
23
24
  import { gojaDevEnvironment, gojaDevUnchangedFiles, gojaEnvironment, goEnvironmentValues, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
25
+ import { prerenderModules } from './skgo-adapter/prerender-modules.js';
24
26
  import { identity } from './skgo-adapter/identity.js';
25
27
  import { checkEndpoints, validateGenerated } from './skgo-adapter/generated.js';
26
28
  import { authorizePrerenderedScripts } from './skgo-adapter/prerender-csp.js';
@@ -58,10 +60,10 @@ const KIT_COMPONENTS = join(
58
60
  * serve a frontend that was built from a different set of Go functions than it
59
61
  * answers.
60
62
  *
61
- * @param {{ out?: string, precompress?: boolean }} [options]
63
+ * @param {{ out?: string, precompress?: boolean, prerenderPackage?: string }} [options]
62
64
  * @returns {import('@sveltejs/kit').Adapter}
63
65
  */
64
- export default function skgo({ out = 'build', precompress = true } = {}) {
66
+ export default function skgo({ out = 'build', precompress = true, prerenderPackage } = {}) {
65
67
  // The engine's bundle is a fourth environment of kit's own build. Kit reads
66
68
  // `vite.plugins.post` while it assembles its config, long before `adapt`
67
69
  // runs, so the plugin that declares the environment has to exist here; the
@@ -73,7 +75,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
73
75
  name: 'skgo-prerender-main-owner',
74
76
  apply: 'build',
75
77
  configResolved(config) {
76
- prerenderOwner = createPrerenderOwner(process.cwd());
78
+ prerenderOwner = createPrerenderOwner(process.cwd(), prerenderPackage);
77
79
  installPrerenderFailureBoundary(config, prerenderOwner);
78
80
  },
79
81
  // Kit captures its app environment in config/configResolved. Vite awaits
@@ -98,6 +100,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
98
100
  emulate() {
99
101
  return {
100
102
  platform: () => ({
103
+ skgoPrerenderEndpoint(event) { return remoteEndpoint(event); },
101
104
  async skgoPrerenderLoad(module, source, event) {
102
105
  return remoteLoad(module, source, event);
103
106
  },
@@ -113,7 +116,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
113
116
  // compiles the SSR bundle in is also declared in `vite dev`, where Go
114
117
  // pulls one transformed module at a time out of it instead. Each plugin
115
118
  // states its own `apply`, so only one of them is ever live.
116
- vite: { plugins: { pre: [prerenderOwnerPlugin], post: [environment.plugin, goja.plugin, gojaDevEnvironment(), gojaDevUnchangedFiles({ out })] } },
119
+ vite: { plugins: { pre: [prerenderOwnerPlugin, prerenderModules()], post: [environment.plugin, goja.plugin, gojaDevEnvironment(), gojaDevUnchangedFiles({ out })] } },
117
120
  async adapt(builder) {
118
121
  let failure;
119
122
  try {
@@ -129,6 +132,9 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
129
132
  checkServerLoads(allServerIds, generated.loads, generated.actions);
130
133
 
131
134
  const endpoints = checkEndpoints(builder, generated.endpoints);
135
+ const prerenderedEndpoints = Object.fromEntries(builder.routes
136
+ .filter((route) => route.prerender === true && endpoints.has(route.id))
137
+ .map((route) => [route.id, endpoints.get(route.id)]));
132
138
 
133
139
  builder.writeClient(`${out}/client`);
134
140
  checkRemoteIds(`${out}/client`, generated.remotes, hashes);
@@ -195,6 +201,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
195
201
  // records in the node module it builds.
196
202
  nodes: serverIds,
197
203
  loads: generated.loads,
204
+ prerenderedEndpoints,
198
205
  actions: [...new Set(generated.actions)].sort(),
199
206
  ssr: describeSSR(builder, kit, nodes),
200
207
  routes: kit._.routes.map((/** @type {any} */ route) => ({