@skgo/sveltekit-adapter 0.19.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +4 -0
- package/package.json +1 -1
- package/skgo-adapter/entry.js +8 -8
- package/skgo-adapter/env.js +4 -0
- package/skgo-adapter/prerender.d.ts +4 -0
- package/skgo-adapter/prerender.js +223 -17
- package/skgo-adapter.js +9 -3
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
package/skgo-adapter/entry.js
CHANGED
|
@@ -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: () =>
|
|
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: () =>
|
|
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:
|
|
183
|
-
isSubRequest:
|
|
184
|
-
isRemoteRequest:
|
|
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 = [];
|
package/skgo-adapter/env.js
CHANGED
|
@@ -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
|
}
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
/** Internal bridge used by generated declared-prerender input modules. */
|
|
2
2
|
export function remoteInputs<Input = unknown>(module: string, name: string): Promise<Input[]>;
|
|
3
|
+
|
|
4
|
+
/** Build-only logical request lifecycle used by generated server 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
|
-
|
|
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,6 +18,7 @@ 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';
|
|
@@ -58,10 +59,10 @@ const KIT_COMPONENTS = join(
|
|
|
58
59
|
* serve a frontend that was built from a different set of Go functions than it
|
|
59
60
|
* answers.
|
|
60
61
|
*
|
|
61
|
-
* @param {{ out?: string, precompress?: boolean }} [options]
|
|
62
|
+
* @param {{ out?: string, precompress?: boolean, prerenderPackage?: string }} [options]
|
|
62
63
|
* @returns {import('@sveltejs/kit').Adapter}
|
|
63
64
|
*/
|
|
64
|
-
export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
65
|
+
export default function skgo({ out = 'build', precompress = true, prerenderPackage } = {}) {
|
|
65
66
|
// The engine's bundle is a fourth environment of kit's own build. Kit reads
|
|
66
67
|
// `vite.plugins.post` while it assembles its config, long before `adapt`
|
|
67
68
|
// runs, so the plugin that declares the environment has to exist here; the
|
|
@@ -73,7 +74,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
73
74
|
name: 'skgo-prerender-main-owner',
|
|
74
75
|
apply: 'build',
|
|
75
76
|
configResolved(config) {
|
|
76
|
-
prerenderOwner = createPrerenderOwner(process.cwd());
|
|
77
|
+
prerenderOwner = createPrerenderOwner(process.cwd(), prerenderPackage);
|
|
77
78
|
installPrerenderFailureBoundary(config, prerenderOwner);
|
|
78
79
|
},
|
|
79
80
|
// Kit captures its app environment in config/configResolved. Vite awaits
|
|
@@ -98,6 +99,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
98
99
|
emulate() {
|
|
99
100
|
return {
|
|
100
101
|
platform: () => ({
|
|
102
|
+
skgoPrerenderEndpoint(event) { return remoteEndpoint(event); },
|
|
101
103
|
async skgoPrerenderLoad(module, source, event) {
|
|
102
104
|
return remoteLoad(module, source, event);
|
|
103
105
|
},
|
|
@@ -129,6 +131,9 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
129
131
|
checkServerLoads(allServerIds, generated.loads, generated.actions);
|
|
130
132
|
|
|
131
133
|
const endpoints = checkEndpoints(builder, generated.endpoints);
|
|
134
|
+
const prerenderedEndpoints = Object.fromEntries(builder.routes
|
|
135
|
+
.filter((route) => route.prerender === true && endpoints.has(route.id))
|
|
136
|
+
.map((route) => [route.id, endpoints.get(route.id)]));
|
|
132
137
|
|
|
133
138
|
builder.writeClient(`${out}/client`);
|
|
134
139
|
checkRemoteIds(`${out}/client`, generated.remotes, hashes);
|
|
@@ -195,6 +200,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
195
200
|
// records in the node module it builds.
|
|
196
201
|
nodes: serverIds,
|
|
197
202
|
loads: generated.loads,
|
|
203
|
+
prerenderedEndpoints,
|
|
198
204
|
actions: [...new Set(generated.actions)].sort(),
|
|
199
205
|
ssr: describeSSR(builder, kit, nodes),
|
|
200
206
|
routes: kit._.routes.map((/** @type {any} */ route) => ({
|