@skgo/sveltekit-adapter 0.10.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/skgo-adapter/env-private.js +48 -0
- package/skgo-adapter/env.js +87 -2
- package/skgo-adapter/generated.js +9 -2
- package/skgo-adapter/prerender-csp.js +19 -0
- package/skgo-adapter.js +128 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skgo/sveltekit-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "SvelteKit adapter for skgo: builds a frontend that a Go binary renders and answers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -32,10 +32,12 @@
|
|
|
32
32
|
"skgo-adapter/app-paths.js",
|
|
33
33
|
"skgo-adapter/app-server.js",
|
|
34
34
|
"skgo-adapter/entry.js",
|
|
35
|
+
"skgo-adapter/env-private.js",
|
|
35
36
|
"skgo-adapter/env.js",
|
|
36
37
|
"skgo-adapter/generated.js",
|
|
37
38
|
"skgo-adapter/identity.js",
|
|
38
|
-
"skgo-adapter/polyfill.js"
|
|
39
|
+
"skgo-adapter/polyfill.js",
|
|
40
|
+
"skgo-adapter/prerender-csp.js"
|
|
39
41
|
],
|
|
40
42
|
"peerDependencies": {
|
|
41
43
|
"@sveltejs/kit": "^3.0.0-next.0"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
function canonical(file) {
|
|
5
|
+
try { return realpathSync.native(file); } catch { return file; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Vite's raw/transform middleware can serve generated server files directly.
|
|
9
|
+
// Deny them before those middleware run, including absolute /@fs requests.
|
|
10
|
+
export function protectEnvironmentFiles({ root, kitOut, output, base = '/', token }) {
|
|
11
|
+
const files = [
|
|
12
|
+
resolve(root, '.svelte-kit/skgo-env-values.js'),
|
|
13
|
+
resolve(root, '.svelte-kit/skgo-env-runtime.json'),
|
|
14
|
+
resolve(root, kitOut, 'generated/build/env/config.js'),
|
|
15
|
+
resolve(root, kitOut, 'generated/dev/env/config.js'),
|
|
16
|
+
resolve(root, output, 'env.json')
|
|
17
|
+
].map(canonical);
|
|
18
|
+
const directories = [
|
|
19
|
+
resolve(root, kitOut, 'output/server'),
|
|
20
|
+
resolve(root, kitOut, 'generated/build/env/private'),
|
|
21
|
+
resolve(root, kitOut, 'generated/dev/env/private'),
|
|
22
|
+
resolve(root, output, 'ssr')
|
|
23
|
+
].map(canonical);
|
|
24
|
+
return (req, res, next) => {
|
|
25
|
+
let path;
|
|
26
|
+
try { path = decodeURIComponent((req.url || '').split('?')[0]); }
|
|
27
|
+
catch { res.statusCode = 400; res.end('Bad request'); return; }
|
|
28
|
+
if (base !== '/' && path.startsWith(base)) path = '/' + path.slice(base.length);
|
|
29
|
+
if (token && /^\/__skgo_dev\/(?:module|styles)(?:\/|$)/.test(path) && req.headers['x-skgo-dev-token'] !== token) {
|
|
30
|
+
res.statusCode = 403; res.end('Forbidden'); return;
|
|
31
|
+
}
|
|
32
|
+
let file;
|
|
33
|
+
if (path.startsWith('/@id/')) {
|
|
34
|
+
const id = path.slice(5).replace('__x00__', '\0');
|
|
35
|
+
if (id === '$app/env/private' || id === '#app/env/private' || /<sveltekit:generated>\/env\/(?:config\.js|private(?:\/|$))/.test(id)) {
|
|
36
|
+
res.statusCode = 403; res.end('Forbidden'); return;
|
|
37
|
+
}
|
|
38
|
+
file = resolve(id);
|
|
39
|
+
} else {
|
|
40
|
+
file = path.startsWith('/@fs/') ? resolve(path.slice(4)) : resolve(root, '.' + path);
|
|
41
|
+
}
|
|
42
|
+
file = canonical(file);
|
|
43
|
+
if (files.includes(file) || directories.some(dir => file === dir || file.startsWith(dir + '/'))) {
|
|
44
|
+
res.statusCode = 403; res.end('Forbidden'); return;
|
|
45
|
+
}
|
|
46
|
+
next();
|
|
47
|
+
};
|
|
48
|
+
}
|
package/skgo-adapter/env.js
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { createRequire } from 'node:module';
|
|
28
|
-
import {
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { randomBytes } from 'node:crypto';
|
|
30
|
+
import { protectEnvironmentFiles } from './env-private.js';
|
|
31
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
|
|
29
32
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
30
33
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
34
|
|
|
@@ -64,7 +67,7 @@ const { transformSync } = await fromApp('vite/rolldown/experimental');
|
|
|
64
67
|
// `fetchModule` operates on the app's own `DevEnvironment`, so it has to be
|
|
65
68
|
// the app's copy for the same reason rolldown is: a second module realm's
|
|
66
69
|
// vite does not recognise this one's environments.
|
|
67
|
-
const { fetchModule, isCSSRequest } = await fromApp('vite');
|
|
70
|
+
const { fetchModule, isCSSRequest, loadEnv } = await fromApp('vite');
|
|
68
71
|
|
|
69
72
|
// These are the same two Kit functions its own dev server calls in
|
|
70
73
|
// `update_manifest`: one derives the route/node graph from the authored tree,
|
|
@@ -324,6 +327,11 @@ export function gojaEnvironment() {
|
|
|
324
327
|
load: {
|
|
325
328
|
order: 'pre',
|
|
326
329
|
handler(id) {
|
|
330
|
+
if (/\/\.svelte-kit\/skgo-env-(?:values\.js|runtime\.json)(?:\?|$)/.test(id)) {
|
|
331
|
+
throw new Error('skgo: environment build inputs are private');
|
|
332
|
+
}
|
|
333
|
+
const environment = goEnvironmentModule(id);
|
|
334
|
+
if (environment !== null) return environment;
|
|
327
335
|
if (!id.startsWith(PREFIX)) return null;
|
|
328
336
|
const make = sources[id.slice(PREFIX.length)];
|
|
329
337
|
if (!make) throw new Error('skgo: no virtual module ' + id.slice(PREFIX.length));
|
|
@@ -926,6 +934,11 @@ export function gojaDevEnvironment({ outDir = '.svelte-kit' } = {}) {
|
|
|
926
934
|
load: {
|
|
927
935
|
order: 'pre',
|
|
928
936
|
handler(id) {
|
|
937
|
+
if (/\/\.svelte-kit\/skgo-env-(?:values\.js|runtime\.json)(?:\?|$)/.test(id)) {
|
|
938
|
+
throw new Error('skgo: environment build inputs are private');
|
|
939
|
+
}
|
|
940
|
+
const environment = goEnvironmentModule(id);
|
|
941
|
+
if (environment !== null) return environment;
|
|
929
942
|
if (!id.startsWith(PREFIX)) return null;
|
|
930
943
|
const make = sources[id.slice(PREFIX.length)];
|
|
931
944
|
if (!make) throw new Error('skgo: no virtual module ' + id.slice(PREFIX.length));
|
|
@@ -1056,3 +1069,75 @@ function errorText(e) {
|
|
|
1056
1069
|
if (e instanceof Error) return e.stack ?? e.message;
|
|
1057
1070
|
return String(e);
|
|
1058
1071
|
}
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
// Values are validated by Go once before Kit loads its environment declaration.
|
|
1075
|
+
// This must be a real file: Kit imports env.ts in a separate Vite server with
|
|
1076
|
+
// configFile:false, where this adapter's virtual modules do not exist.
|
|
1077
|
+
export function goEnvironmentValues(output = 'build') {
|
|
1078
|
+
let token = '';
|
|
1079
|
+
let kitOut = '.svelte-kit';
|
|
1080
|
+
let fields = [];
|
|
1081
|
+
let values = {};
|
|
1082
|
+
return {
|
|
1083
|
+
plugin: {
|
|
1084
|
+
name: 'skgo-environment-values',
|
|
1085
|
+
configureServer(server) {
|
|
1086
|
+
// A server-only build input can contain private values. Vite's
|
|
1087
|
+
// filesystem endpoint must not make it downloadable in development.
|
|
1088
|
+
server.middlewares.use(protectEnvironmentFiles({
|
|
1089
|
+
root: server.config.root, kitOut, output, base: server.config.base, token
|
|
1090
|
+
}));
|
|
1091
|
+
},
|
|
1092
|
+
configResolved(config) {
|
|
1093
|
+
const schema = join(config.root, 'skgo.env.json');
|
|
1094
|
+
if (!existsSync(schema)) return;
|
|
1095
|
+
fields = JSON.parse(readFileSync(schema, 'utf8')).fields;
|
|
1096
|
+
const setup = config.plugins.find((p) => p.name === 'vite-plugin-sveltekit-setup');
|
|
1097
|
+
const kit = setup?.api?.options;
|
|
1098
|
+
if (!kit) throw new Error('skgo: Kit environment configuration is unavailable');
|
|
1099
|
+
kitOut = kit.outDir;
|
|
1100
|
+
const raw = loadEnv(config.mode, resolve(config.root, kit.env.dir), '');
|
|
1101
|
+
// Pass only declared values; diagnostics never echo the full process env.
|
|
1102
|
+
const input = Object.fromEntries(fields.filter((f) => f.name in raw).map((f) => [f.name, raw[f.name]]));
|
|
1103
|
+
try {
|
|
1104
|
+
values = JSON.parse(execFileSync('go', ['tool', 'skgo', 'env', '--schema', schema], {
|
|
1105
|
+
cwd: config.root, input: JSON.stringify(input), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
|
1106
|
+
}));
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new Error('skgo: Go environment validation failed\n' + String(error.stderr || error.message));
|
|
1109
|
+
}
|
|
1110
|
+
const file = join(config.root, '.svelte-kit/skgo-env-values.js');
|
|
1111
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
1112
|
+
writeFileSync(file, 'export const values = ' + JSON.stringify(values) + ';\n', { mode: 0o600 });
|
|
1113
|
+
chmodSync(file, 0o600);
|
|
1114
|
+
|
|
1115
|
+
if (config.command === 'serve') {
|
|
1116
|
+
token = randomBytes(32).toString('hex');
|
|
1117
|
+
const artifact = { token, fields, static: Object.fromEntries(fields.filter((f) => f.static && f.name in values).map((f) => [f.name, values[f.name]])), values: input };
|
|
1118
|
+
const runtimeFile = join(config.root, '.svelte-kit/skgo-env-runtime.json');
|
|
1119
|
+
writeFileSync(runtimeFile, JSON.stringify(artifact), { mode: 0o600 });
|
|
1120
|
+
chmodSync(runtimeFile, 0o600);
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
artifact() {
|
|
1125
|
+
return { fields, static: Object.fromEntries(fields.filter((f) => f.static && f.name in values).map((f) => [f.name, values[f.name]])) };
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Kit's public/private modules snapshot these properties during evaluation.
|
|
1131
|
+
// Go installs the snapshot before loading any application module. In particular,
|
|
1132
|
+
// importing Kit's original config here would re-run JavaScript validators and
|
|
1133
|
+
// bake the development/build environment into newly-created renderer runtimes.
|
|
1134
|
+
function goEnvironmentModule(id) {
|
|
1135
|
+
if (!existsSync(join(process.cwd(), 'skgo.env.json'))) return null;
|
|
1136
|
+
if (!/\/generated\/(?:build|dev)\/env\/config\.js$/.test(id.replaceAll('\\', '/'))) return null;
|
|
1137
|
+
return `const snapshot = globalThis.__skgo_environment || {};
|
|
1138
|
+
export const dynamic_private_env = snapshot.private || {};
|
|
1139
|
+
export const explicit_public_env = snapshot.public || {};
|
|
1140
|
+
export const rendered_env = snapshot.dynamicPublic || {};
|
|
1141
|
+
export const variables = {};
|
|
1142
|
+
export function set_env() {}`;
|
|
1143
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* first-class.
|
|
11
11
|
*
|
|
12
12
|
* @param {any} parsed the parsed contents of `skgo.remotes.json`
|
|
13
|
-
* @returns {{ remotes: string[], loads: string[], actions: string[], endpoints: Record<string, string[]
|
|
13
|
+
* @returns {{ remotes: string[], loads: string[], actions: string[], endpoints: Record<string, string[]>, prerender?: { root: string, package: string } }}
|
|
14
14
|
*/
|
|
15
15
|
export function validateGenerated(parsed) {
|
|
16
16
|
if (!Array.isArray(parsed.remotes)) {
|
|
@@ -37,6 +37,13 @@ export function validateGenerated(parsed) {
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
if (parsed.loads.length > 0 && (
|
|
41
|
+
typeof parsed.prerender?.root !== 'string' ||
|
|
42
|
+
typeof parsed.prerender?.package !== 'string' ||
|
|
43
|
+
!parsed.prerender.package.startsWith('./')
|
|
44
|
+
)) {
|
|
45
|
+
throw new Error('skgo: generated Go loads need a build-time prerender command; run `go generate ./...`.');
|
|
46
|
+
}
|
|
40
47
|
if (!Array.isArray(parsed.actions)) {
|
|
41
48
|
throw new Error('skgo: skgo.remotes.json has no `actions` array. Run `go generate ./...` before building the frontend.');
|
|
42
49
|
}
|
|
@@ -57,7 +64,7 @@ export function validateGenerated(parsed) {
|
|
|
57
64
|
);
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
|
-
return { remotes: parsed.remotes, loads: parsed.loads, actions: parsed.actions, endpoints: parsed.endpoints };
|
|
67
|
+
return { remotes: parsed.remotes, loads: parsed.loads, actions: parsed.actions, endpoints: parsed.endpoints, prerender: parsed.prerender };
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// Kit writes streamed promise settlements after the closing HTML tag. Its
|
|
5
|
+
// prerendered CSP meta tag hashes the boot script but does not hash those later
|
|
6
|
+
// inline scripts. Add only the hashes of scripts present in the finished file.
|
|
7
|
+
export function authorizePrerenderedScripts(file) {
|
|
8
|
+
const html = readFileSync(file, 'utf8');
|
|
9
|
+
const meta = /<meta http-equiv="content-security-policy" content="([^"]*)">/i.exec(html);
|
|
10
|
+
if (!meta) return;
|
|
11
|
+
const additions = [];
|
|
12
|
+
for (const [, attributes, body] of html.matchAll(/<script([^>]*)>([\s\S]*?)<\/script>/gi)) {
|
|
13
|
+
if (/\bsrc\s*=/.test(attributes)) continue;
|
|
14
|
+
const hash = `'sha256-${createHash('sha256').update(body).digest('base64')}'`;
|
|
15
|
+
if (!meta[1].includes(hash)) additions.push(hash);
|
|
16
|
+
}
|
|
17
|
+
if (!additions.length) return;
|
|
18
|
+
writeFileSync(file, html.replace(meta[0], meta[0].replace(meta[1], `${meta[1]} ${[...new Set(additions)].join(' ')}`)));
|
|
19
|
+
}
|
package/skgo-adapter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
|
+
mkdtempSync,
|
|
3
4
|
mkdirSync,
|
|
4
5
|
readdirSync,
|
|
5
6
|
readFileSync,
|
|
@@ -8,11 +9,33 @@ import {
|
|
|
8
9
|
statSync,
|
|
9
10
|
writeFileSync
|
|
10
11
|
} from 'node:fs';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
11
15
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
12
16
|
import { pathToFileURL } from 'node:url';
|
|
13
|
-
import { gojaDevEnvironment, gojaEnvironment, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
|
|
17
|
+
import { gojaDevEnvironment, gojaEnvironment, goEnvironmentValues, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
|
|
14
18
|
import { identity } from './skgo-adapter/identity.js';
|
|
15
19
|
import { checkEndpoints, validateGenerated } from './skgo-adapter/generated.js';
|
|
20
|
+
import { authorizePrerenderedScripts } from './skgo-adapter/prerender-csp.js';
|
|
21
|
+
|
|
22
|
+
function runPrerenderCommand(command, args, cwd, input = '') {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
25
|
+
let stdout = '';
|
|
26
|
+
let stderr = '';
|
|
27
|
+
child.stdout.setEncoding('utf8');
|
|
28
|
+
child.stderr.setEncoding('utf8');
|
|
29
|
+
child.stdout.on('data', (chunk) => (stdout += chunk));
|
|
30
|
+
child.stderr.on('data', (chunk) => (stderr += chunk));
|
|
31
|
+
child.on('error', reject);
|
|
32
|
+
child.on('close', (code) => {
|
|
33
|
+
if (code === 0) resolve(stdout);
|
|
34
|
+
else reject(new Error(`skgo prerender command exited ${code}: ${stderr}`));
|
|
35
|
+
});
|
|
36
|
+
child.stdin.end(input);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
16
39
|
|
|
17
40
|
// Which skgo this adapter is: the version this package was published at, and a
|
|
18
41
|
// fingerprint taken over its own files. The Go that reads the manifest below
|
|
@@ -56,26 +79,114 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
56
79
|
// runs, so the plugin that declares the environment has to exist here; the
|
|
57
80
|
// build itself waits until adapt() knows the node table.
|
|
58
81
|
const goja = gojaEnvironment();
|
|
82
|
+
const environment = goEnvironmentValues(out);
|
|
83
|
+
let prerenderBinary;
|
|
84
|
+
let prerenderRoot;
|
|
85
|
+
let prerenderBuild;
|
|
86
|
+
let unflatten;
|
|
87
|
+
let transportDecoders = {};
|
|
88
|
+
async function buildPrerenderBinary() {
|
|
89
|
+
if (!prerenderBuild) {
|
|
90
|
+
prerenderBuild = (async () => {
|
|
91
|
+
const generated = readGenerated();
|
|
92
|
+
if (!generated.prerender) throw new Error('skgo: generated prerender command is missing');
|
|
93
|
+
prerenderRoot = resolve(process.cwd(), generated.prerender.root);
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), 'skgo-prerender-'));
|
|
95
|
+
prerenderBinary = join(dir, 'loads');
|
|
96
|
+
await runPrerenderCommand('go', ['build', '-o', prerenderBinary, generated.prerender.package], prerenderRoot);
|
|
97
|
+
process.on('exit', () => rmSync(dir, { recursive: true, force: true }));
|
|
98
|
+
const fromKit = createRequire(realpathSync(join(process.cwd(), 'node_modules/@sveltejs/kit/package.json')));
|
|
99
|
+
({ unflatten } = await import(pathToFileURL(fromKit.resolve('devalue')).href));
|
|
100
|
+
const hooks = join(process.cwd(), '.svelte-kit/output/server/entries/hooks.universal.js');
|
|
101
|
+
if (existsSync(hooks)) {
|
|
102
|
+
const { transport = {} } = await import(pathToFileURL(hooks).href);
|
|
103
|
+
transportDecoders = Object.fromEntries(Object.entries(transport).map(([key, entry]) => [key, entry.decode]));
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
}
|
|
107
|
+
await prerenderBuild;
|
|
108
|
+
}
|
|
59
109
|
|
|
60
110
|
return {
|
|
61
111
|
name: 'skgo',
|
|
112
|
+
emulate() {
|
|
113
|
+
return {
|
|
114
|
+
platform: () => ({
|
|
115
|
+
async skgoPrerenderLoad(module, event) {
|
|
116
|
+
await buildPrerenderBinary();
|
|
117
|
+
const headers = Object.fromEntries(
|
|
118
|
+
[...event.request.headers].map(([name, value]) => [name, [value]])
|
|
119
|
+
);
|
|
120
|
+
const cookies = event.cookies.getAll();
|
|
121
|
+
if (cookies.length) {
|
|
122
|
+
headers.cookie = [cookies.map(({ name, value }) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`).join('; ')];
|
|
123
|
+
}
|
|
124
|
+
const request = {
|
|
125
|
+
module,
|
|
126
|
+
url: event.url.href,
|
|
127
|
+
routeId: event.route.id,
|
|
128
|
+
params: Object.fromEntries(Object.entries(event.params)),
|
|
129
|
+
parent: await event.parent(),
|
|
130
|
+
headers
|
|
131
|
+
};
|
|
132
|
+
const raw = await runPrerenderCommand(prerenderBinary, [], prerenderRoot, JSON.stringify(request));
|
|
133
|
+
const answer = JSON.parse(raw);
|
|
134
|
+
for (const [name, values] of Object.entries(answer.headers ?? {})) {
|
|
135
|
+
event.setHeaders({ [name]: values.join(', ') });
|
|
136
|
+
}
|
|
137
|
+
for (const cookie of answer.cookies ?? []) {
|
|
138
|
+
event.cookies.set(cookie.name, cookie.value, {
|
|
139
|
+
path: cookie.path,
|
|
140
|
+
...(cookie.domain ? { domain: cookie.domain } : {}),
|
|
141
|
+
...(cookie.maxAge ? { maxAge: cookie.maxAge } : {}),
|
|
142
|
+
httpOnly: cookie.httpOnly,
|
|
143
|
+
secure: cookie.secure,
|
|
144
|
+
sameSite: ['lax', 'lax', 'strict', 'none'][cookie.sameSite] ?? 'lax'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (answer.data) {
|
|
148
|
+
const pending = new Map();
|
|
149
|
+
const revivers = {
|
|
150
|
+
...transportDecoders,
|
|
151
|
+
Promise: (id) => {
|
|
152
|
+
let resolve;
|
|
153
|
+
let reject;
|
|
154
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
155
|
+
pending.set(id, { promise, resolve, reject });
|
|
156
|
+
return promise;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
answer.data = unflatten(answer.data, revivers);
|
|
160
|
+
for (const chunk of answer.chunks ?? []) {
|
|
161
|
+
const deferred = pending.get(chunk.id);
|
|
162
|
+
if (!deferred) throw new Error(`skgo: unknown prerender deferred id ${chunk.id}`);
|
|
163
|
+
if (chunk.error) deferred.reject(new Error(chunk.error));
|
|
164
|
+
else deferred.resolve(unflatten(chunk.data, revivers));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return answer;
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
},
|
|
62
172
|
// Both halves of the same environment. Kit hands `vite.plugins.post` to
|
|
63
173
|
// vite unconditionally — it is a member of the plugin array kit returns
|
|
64
174
|
// whether it is building or serving — so the environment the build
|
|
65
175
|
// compiles the SSR bundle in is also declared in `vite dev`, where Go
|
|
66
176
|
// pulls one transformed module at a time out of it instead. Each plugin
|
|
67
177
|
// states its own `apply`, so only one of them is ever live.
|
|
68
|
-
vite: { plugins: { post: [goja.plugin, gojaDevEnvironment()] } },
|
|
178
|
+
vite: { plugins: { post: [environment.plugin, goja.plugin, gojaDevEnvironment()] } },
|
|
69
179
|
async adapt(builder) {
|
|
70
180
|
rmSync(out, { force: true, recursive: true });
|
|
71
181
|
|
|
182
|
+
write(`${out}/env.json`, JSON.stringify(environment.artifact()));
|
|
72
183
|
const generated = readGenerated();
|
|
73
184
|
const { manifest: kit, source } = await readKitManifest(builder);
|
|
74
185
|
const hashes = checkRemoteHashes(kit, generated.remotes);
|
|
75
186
|
|
|
76
|
-
const nodes = await readNodes(builder, source, kit);
|
|
187
|
+
const { nodes, allServerIds } = await readNodes(builder, source, kit);
|
|
77
188
|
const serverIds = nodes.map((node) => node.server);
|
|
78
|
-
checkServerLoads(
|
|
189
|
+
checkServerLoads(allServerIds, generated.loads, generated.actions);
|
|
79
190
|
|
|
80
191
|
const endpoints = checkEndpoints(builder, generated.endpoints);
|
|
81
192
|
|
|
@@ -83,6 +194,9 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
83
194
|
checkRemoteIds(`${out}/client`, generated.remotes, hashes);
|
|
84
195
|
|
|
85
196
|
builder.writePrerendered(`${out}/prerendered`);
|
|
197
|
+
for (const file of walk(`${out}/prerendered`)) {
|
|
198
|
+
if (file.endsWith('.html')) authorizePrerenderedScripts(file);
|
|
199
|
+
}
|
|
86
200
|
const prerendered = readPrerendered(builder);
|
|
87
201
|
// The SPA shell is still emitted: it is what a page whose branch turns
|
|
88
202
|
// SSR off is answered with.
|
|
@@ -205,7 +319,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
205
319
|
* halves were never checked against each other. The shape checks themselves
|
|
206
320
|
* live in `skgo-adapter/generated.js`, beside the paths they validate.
|
|
207
321
|
*
|
|
208
|
-
* @returns {{ remotes: string[], loads: string[], actions: string[], endpoints: Record<string, string[]
|
|
322
|
+
* @returns {{ remotes: string[], loads: string[], actions: string[], endpoints: Record<string, string[]>, prerender?: { root: string, package: string } }}
|
|
209
323
|
*/
|
|
210
324
|
function readGenerated() {
|
|
211
325
|
let raw;
|
|
@@ -354,7 +468,7 @@ async function readKitManifest(builder) {
|
|
|
354
468
|
* @param {import('@sveltejs/kit').Builder} builder
|
|
355
469
|
* @param {string} source kit's generated manifest, as text
|
|
356
470
|
* @param {any} kit the same manifest, imported
|
|
357
|
-
* @returns {Promise<Node[]>}
|
|
471
|
+
* @returns {Promise<{ nodes: Node[], allServerIds: string[] }>}
|
|
358
472
|
*/
|
|
359
473
|
async function readNodes(builder, source, kit) {
|
|
360
474
|
const dir = join(builder.getServerDirectory(), 'nodes');
|
|
@@ -382,6 +496,11 @@ async function readNodes(builder, source, kit) {
|
|
|
382
496
|
|
|
383
497
|
/** @type {Node[]} */
|
|
384
498
|
const nodes = [];
|
|
499
|
+
const allServerIds = [];
|
|
500
|
+
for (const file of files.values()) {
|
|
501
|
+
const module = await import(pathToFileURL(file).href);
|
|
502
|
+
if (module.server_id) allServerIds.push(module.server_id);
|
|
503
|
+
}
|
|
385
504
|
for (const index of order) {
|
|
386
505
|
const file = files.get(index);
|
|
387
506
|
if (!file) {
|
|
@@ -402,7 +521,7 @@ async function readNodes(builder, source, kit) {
|
|
|
402
521
|
});
|
|
403
522
|
}
|
|
404
523
|
|
|
405
|
-
return nodes;
|
|
524
|
+
return { nodes, allServerIds };
|
|
406
525
|
}
|
|
407
526
|
|
|
408
527
|
/**
|
|
@@ -645,9 +764,8 @@ function* walk(dir) {
|
|
|
645
764
|
/**
|
|
646
765
|
* The same check for server loads. Kit decides whether its client asks for
|
|
647
766
|
* `__data.json` at all by looking for a `load` export in the *built*
|
|
648
|
-
* `+*.server.js
|
|
649
|
-
*
|
|
650
|
-
* data nobody answers.
|
|
767
|
+
* `+*.server.js`. A prerendered leaf is removed from the final manifest, so
|
|
768
|
+
* check all node modules Kit compiled, including the removed leaves.
|
|
651
769
|
*
|
|
652
770
|
* @param {string[]} nodes
|
|
653
771
|
* @param {string[]} loads
|