@skgo/sveltekit-adapter 0.2.7
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/LICENSE +21 -0
- package/README.md +39 -0
- package/index.d.ts +26 -0
- package/package.json +42 -0
- package/skgo-adapter/app-paths.js +40 -0
- package/skgo-adapter/app-server.js +152 -0
- package/skgo-adapter/entry.js +420 -0
- package/skgo-adapter/env.js +1044 -0
- package/skgo-adapter/identity.js +75 -0
- package/skgo-adapter/polyfill.js +157 -0
- package/skgo-adapter.js +793 -0
package/skgo-adapter.js
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { gojaDevEnvironment, gojaEnvironment, nodeTable, SSR_TARGET } from './skgo-adapter/env.js';
|
|
14
|
+
import { identity } from './skgo-adapter/identity.js';
|
|
15
|
+
|
|
16
|
+
// Which skgo this adapter is: the version this package was published at, and a
|
|
17
|
+
// fingerprint taken over its own files. The Go that reads the manifest below
|
|
18
|
+
// takes the same fingerprint over the copy it embeds and refuses a build whose
|
|
19
|
+
// adapter is not its own, so an install that has fallen behind the binary is
|
|
20
|
+
// named at startup instead of failing later as something unrelated.
|
|
21
|
+
const SKGO = identity();
|
|
22
|
+
|
|
23
|
+
// Kit supplies these two components when an app does not author its own root
|
|
24
|
+
// layout or error boundary (`create_manifest_data`, "fallback root layout and
|
|
25
|
+
// root error components"). Their source names deliberately lack the route-file
|
|
26
|
+
// `+` prefix, so recognise only the exact files from the app's installed Kit.
|
|
27
|
+
const KIT_COMPONENTS = join(
|
|
28
|
+
realpathSync(join(process.cwd(), 'node_modules/@sveltejs/kit')),
|
|
29
|
+
'src/runtime/components'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The skgo adapter. It emits everything the Go binary embeds and nothing else:
|
|
34
|
+
* the client bundle, kit's own SPA boot document, the SSR bundle the Go process
|
|
35
|
+
* renders pages with, and a manifest describing what Go must answer.
|
|
36
|
+
*
|
|
37
|
+
* The SSR bundle is the only JavaScript that runs in production, and it does no
|
|
38
|
+
* I/O: it is kit's own root component and Svelte's renderer, with every remote
|
|
39
|
+
* function's body replaced by a call back into Go.
|
|
40
|
+
*
|
|
41
|
+
* It also carries the remote-function ids and the server-load module paths
|
|
42
|
+
* forward. `skgo generate` writes `skgo.remotes.json` beside this file when it
|
|
43
|
+
* emits the `.remote.ts` modules and the `+*.server.ts` stubs; the adapter
|
|
44
|
+
* checks that kit really compiled those modules and copies the lists into the
|
|
45
|
+
* build, so the Go binary can refuse to serve a frontend that was built from a
|
|
46
|
+
* different set of Go functions than it answers.
|
|
47
|
+
*
|
|
48
|
+
* @param {{ out?: string, precompress?: boolean }} [options]
|
|
49
|
+
* @returns {import('@sveltejs/kit').Adapter}
|
|
50
|
+
*/
|
|
51
|
+
export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
52
|
+
// The engine's bundle is a fourth environment of kit's own build. Kit reads
|
|
53
|
+
// `vite.plugins.post` while it assembles its config, long before `adapt`
|
|
54
|
+
// runs, so the plugin that declares the environment has to exist here; the
|
|
55
|
+
// build itself waits until adapt() knows the node table.
|
|
56
|
+
const goja = gojaEnvironment();
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
name: 'skgo',
|
|
60
|
+
// Both halves of the same environment. Kit hands `vite.plugins.post` to
|
|
61
|
+
// vite unconditionally — it is a member of the plugin array kit returns
|
|
62
|
+
// whether it is building or serving — so the environment the build
|
|
63
|
+
// compiles the SSR bundle in is also declared in `vite dev`, where Go
|
|
64
|
+
// pulls one transformed module at a time out of it instead. Each plugin
|
|
65
|
+
// states its own `apply`, so only one of them is ever live.
|
|
66
|
+
vite: { plugins: { post: [goja.plugin, gojaDevEnvironment()] } },
|
|
67
|
+
async adapt(builder) {
|
|
68
|
+
rmSync(out, { force: true, recursive: true });
|
|
69
|
+
|
|
70
|
+
const generated = readGenerated();
|
|
71
|
+
const { manifest: kit, source } = await readKitManifest(builder);
|
|
72
|
+
const hashes = checkRemoteHashes(kit, generated.remotes);
|
|
73
|
+
|
|
74
|
+
const nodes = await readNodes(builder, source, kit);
|
|
75
|
+
const serverIds = nodes.map((node) => node.server);
|
|
76
|
+
checkServerLoads(serverIds, generated.loads);
|
|
77
|
+
|
|
78
|
+
const endpoints = checkEndpoints(builder, generated.endpoints);
|
|
79
|
+
|
|
80
|
+
builder.writeClient(`${out}/client`);
|
|
81
|
+
checkRemoteIds(`${out}/client`, generated.remotes, hashes);
|
|
82
|
+
|
|
83
|
+
builder.writePrerendered(`${out}/prerendered`);
|
|
84
|
+
const prerendered = readPrerendered(builder);
|
|
85
|
+
// The SPA shell is still emitted: it is what a page whose branch turns
|
|
86
|
+
// SSR off is answered with.
|
|
87
|
+
await builder.generateFallback(`${out}/index.html`);
|
|
88
|
+
|
|
89
|
+
// The document template, verbatim. Go substitutes `%sveltekit.head%`
|
|
90
|
+
// and `%sveltekit.body%` into it the way kit's compiled template
|
|
91
|
+
// function does, so the file a developer edits is the file that is
|
|
92
|
+
// served.
|
|
93
|
+
write(`${out}/app.html`, readFileSync(builder.config.files.appTemplate, 'utf-8'));
|
|
94
|
+
// Kit's last-resort document, for the request whose error cannot be
|
|
95
|
+
// rendered at all — an error in the root layout, or an engine that
|
|
96
|
+
// failed. `respond_with_error` falls to it and so does Go
|
|
97
|
+
// (`runtime/server/errors.js`, `static_error_page`).
|
|
98
|
+
write(`${out}/error.html`, readErrorTemplate(builder));
|
|
99
|
+
await goja.build(
|
|
100
|
+
{
|
|
101
|
+
nodes: nodeTable(process.cwd(), nodes),
|
|
102
|
+
hooks: universalHooks(builder),
|
|
103
|
+
log: (message) => builder.log.minor(message)
|
|
104
|
+
},
|
|
105
|
+
`${out}/ssr/bundle.js`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Kit's own `builder.compress` writes a `.br` and a `.gz` beside every
|
|
109
|
+
// file whose extension it compresses, and Go chooses one per request
|
|
110
|
+
// from Accept-Encoding. Same contract adapter-node ships with its
|
|
111
|
+
// `precompress: true` default.
|
|
112
|
+
if (precompress) {
|
|
113
|
+
await builder.compress(`${out}/client`);
|
|
114
|
+
await builder.compress(`${out}/prerendered`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// `builder` has no writeJson in kit 3.0.0-next.25 (it existed on the
|
|
118
|
+
// kit 2 line); write the manifest ourselves.
|
|
119
|
+
write(
|
|
120
|
+
`${out}/skgo.manifest.json`,
|
|
121
|
+
JSON.stringify(
|
|
122
|
+
{
|
|
123
|
+
// Which skgo wrote this. `ReadManifest` refuses a build
|
|
124
|
+
// whose adapter is not the one the reading module carries.
|
|
125
|
+
skgo: SKGO.version,
|
|
126
|
+
skgoAdapter: SKGO.adapter,
|
|
127
|
+
appDir: builder.config.appDir,
|
|
128
|
+
base: builder.config.paths.base,
|
|
129
|
+
version: builder.config.version.name,
|
|
130
|
+
// One entry per node, positionally: the vite-root-relative
|
|
131
|
+
// path of its `+*.server.ts`, or "" for a node that has
|
|
132
|
+
// none. It is how Go finds the load that answers a slot of
|
|
133
|
+
// a route's branch, and it is the same key kit itself
|
|
134
|
+
// records in the node module it builds.
|
|
135
|
+
nodes: serverIds,
|
|
136
|
+
ssr: describeSSR(builder, kit, nodes),
|
|
137
|
+
routes: kit._.routes.map((/** @type {any} */ route) => ({
|
|
138
|
+
id: route.id,
|
|
139
|
+
pattern: route.pattern.source,
|
|
140
|
+
params: route.params,
|
|
141
|
+
// The methods kit's build says the compiled `+server.ts`
|
|
142
|
+
// exports. Without it a route that is an endpoint and
|
|
143
|
+
// nothing else is indistinguishable from a page route, and
|
|
144
|
+
// Go answered it with the boot document at HTTP 200.
|
|
145
|
+
endpoint: endpoints.has(route.id)
|
|
146
|
+
? { methods: endpoints.get(route.id) }
|
|
147
|
+
: null,
|
|
148
|
+
page: route.page
|
|
149
|
+
? {
|
|
150
|
+
// `[...layouts, leaf]` is the branch the
|
|
151
|
+
// client's `x-sveltekit-invalidated` string
|
|
152
|
+
// is positional over. A hole in `layouts` is
|
|
153
|
+
// a slot no layout fills; it travels as -1
|
|
154
|
+
// because JSON has no holes.
|
|
155
|
+
layouts: Array.from(
|
|
156
|
+
{ length: route.page.layouts.length },
|
|
157
|
+
(_, i) => route.page.layouts[i] ?? -1
|
|
158
|
+
),
|
|
159
|
+
// The `+error.svelte` declared at each layout
|
|
160
|
+
// depth, or -1 where none is. Kit walks this
|
|
161
|
+
// list outward from the node that failed to
|
|
162
|
+
// pick which error page renders and how much
|
|
163
|
+
// of the branch survives with it
|
|
164
|
+
// (`runtime/error-chain.js`). Without it every
|
|
165
|
+
// error would fall to the root error page and
|
|
166
|
+
// lose its layouts.
|
|
167
|
+
errors: Array.from(
|
|
168
|
+
{ length: route.page.layouts.length },
|
|
169
|
+
(_, i) => route.page.errors[i] ?? -1
|
|
170
|
+
),
|
|
171
|
+
leaf: route.page.leaf
|
|
172
|
+
}
|
|
173
|
+
: null
|
|
174
|
+
})),
|
|
175
|
+
remotes: generated.remotes,
|
|
176
|
+
prerendered,
|
|
177
|
+
precompressed: precompress
|
|
178
|
+
},
|
|
179
|
+
null,
|
|
180
|
+
'\t'
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
builder.log.minor(`skgo: wrote ${out}/`);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Reads what `skgo generate` emitted.
|
|
191
|
+
*
|
|
192
|
+
* A missing or shapeless file is a failure, not an empty list: an adapter that
|
|
193
|
+
* quietly compares nothing to nothing reports success for an app whose two
|
|
194
|
+
* halves were never checked against each other.
|
|
195
|
+
*
|
|
196
|
+
* @returns {{ remotes: string[], loads: string[], endpoints: Record<string, string[]> }}
|
|
197
|
+
*/
|
|
198
|
+
function readGenerated() {
|
|
199
|
+
let raw;
|
|
200
|
+
try {
|
|
201
|
+
raw = readFileSync('skgo.remotes.json', 'utf-8');
|
|
202
|
+
} catch {
|
|
203
|
+
throw new Error(
|
|
204
|
+
'skgo: skgo.remotes.json is missing. Run `go generate ./...` before building the frontend.'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const parsed = JSON.parse(raw);
|
|
208
|
+
if (!Array.isArray(parsed.remotes)) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
'skgo: skgo.remotes.json has no `remotes` array. Run `go generate ./...` before building the frontend.'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
for (const id of parsed.remotes) {
|
|
214
|
+
if (typeof id !== 'string' || !/^[^/]+\/[^/]+$/.test(id)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`skgo: skgo.remotes.json lists ${JSON.stringify(id)}, which is not a <hash>/<name> id.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!Array.isArray(parsed.loads)) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
'skgo: skgo.remotes.json has no `loads` array. Run `go generate ./...` before building the frontend.'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
for (const module of parsed.loads) {
|
|
226
|
+
if (typeof module !== 'string' || !/\/\+(page|layout)\.server\.ts$/.test(module)) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`skgo: skgo.remotes.json lists ${JSON.stringify(module)}, which is not a +page.server.ts or +layout.server.ts path.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (typeof parsed.endpoints !== 'object' || parsed.endpoints === null || Array.isArray(parsed.endpoints)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
'skgo: skgo.remotes.json has no `endpoints` object. Run `go generate ./...` before building the frontend.'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
for (const [id, methods] of Object.entries(parsed.endpoints)) {
|
|
238
|
+
if (!id.startsWith('/') || !Array.isArray(methods) || methods.length === 0) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`skgo: skgo.remotes.json maps ${JSON.stringify(id)} to ${JSON.stringify(methods)}, which is not a route id and its methods.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { remotes: parsed.remotes, loads: parsed.loads, endpoints: parsed.endpoints };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The same check for server routes, against the one place kit reports what it
|
|
249
|
+
* compiled: `builder.routes[].api.methods`, which kit derives by importing each
|
|
250
|
+
* built `+server.js` and reading its exports
|
|
251
|
+
* (packages/kit/src/core/postbuild/analyse.js, `analyse_endpoint`). A `fallback`
|
|
252
|
+
* export travels there as `'*'`, and `skgo generate` writes the same spelling,
|
|
253
|
+
* so the two lists are compared literally.
|
|
254
|
+
*
|
|
255
|
+
* This is the check that makes a hand-written `+server.ts` fail the build rather
|
|
256
|
+
* than 404 in the browser: kit would compile it, Go would never have been told
|
|
257
|
+
* about it, and the route would answer nothing.
|
|
258
|
+
*
|
|
259
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
260
|
+
* @param {Record<string, string[]>} declared
|
|
261
|
+
* @returns {Map<string, string[]>} the methods kit compiled, per route id
|
|
262
|
+
*/
|
|
263
|
+
function checkEndpoints(builder, declared) {
|
|
264
|
+
/** @type {Map<string, string[]>} */
|
|
265
|
+
const built = new Map();
|
|
266
|
+
for (const route of builder.routes) {
|
|
267
|
+
if (route.api.methods.length > 0) built.set(route.id, [...route.api.methods].sort());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @type {string[]} */
|
|
271
|
+
const problems = [];
|
|
272
|
+
for (const [id, methods] of Object.entries(declared)) {
|
|
273
|
+
const compiled = built.get(id);
|
|
274
|
+
if (!compiled) {
|
|
275
|
+
problems.push(` generated but not compiled: ${methods.join(', ')} ${id}`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const want = [...methods].sort().join(', ');
|
|
279
|
+
const got = compiled.join(', ');
|
|
280
|
+
if (want !== got) {
|
|
281
|
+
problems.push(` ${id}: Go answers ${want}, the built +server.ts exports ${got}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
for (const [id, methods] of built) {
|
|
285
|
+
if (!(id in declared)) {
|
|
286
|
+
problems.push(` compiled but not generated: ${methods.join(', ')} ${id}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (problems.length) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
'skgo: skgo.remotes.json does not describe the server routes kit just compiled.\n' +
|
|
293
|
+
problems.join('\n') +
|
|
294
|
+
'\n Every server route is written in Go. Run `go generate ./...`.'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return built;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The pathnames the build wrote a file for, exactly as kit records them:
|
|
302
|
+
* percent-decoded and carrying the configured base. Go serves the prerendered
|
|
303
|
+
* tree off this list and issues the trailing-slash 308 off it, which is what
|
|
304
|
+
* adapter-node does with the same array.
|
|
305
|
+
*
|
|
306
|
+
* A prerendered *redirect* is refused rather than dropped. Kit records one when
|
|
307
|
+
* a page it rendered redirected somewhere, and skgo has nothing that would
|
|
308
|
+
* replay it — an app that produced one would silently lose it.
|
|
309
|
+
*
|
|
310
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
311
|
+
* @returns {string[]}
|
|
312
|
+
*/
|
|
313
|
+
function readPrerendered(builder) {
|
|
314
|
+
if (builder.prerendered.redirects.size > 0) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
'skgo: the build prerendered a redirect, which skgo does not serve:\n' +
|
|
317
|
+
[...builder.prerendered.redirects]
|
|
318
|
+
.map(([from, { status, location }]) => ` ${from} -> ${status} ${location}`)
|
|
319
|
+
.join('\n')
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return [...builder.prerendered.paths].sort();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Kit's own server manifest, as an object *and* as text. `generateManifest`
|
|
327
|
+
* returns the source of a module whose only imports sit inside lazy thunks, so
|
|
328
|
+
* importing it resolves nothing and runs no application code — but the source
|
|
329
|
+
* is worth keeping, because the thunks carry information the imported object
|
|
330
|
+
* has already hidden inside closures. See readNodes.
|
|
331
|
+
*
|
|
332
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
333
|
+
* @returns {Promise<{ manifest: any, source: string }>}
|
|
334
|
+
*/
|
|
335
|
+
async function readKitManifest(builder) {
|
|
336
|
+
const dir = builder.getBuildDirectory('skgo');
|
|
337
|
+
const file = join(dir, 'kit-manifest.js');
|
|
338
|
+
const source = builder.generateManifest({ relativePath: '.' });
|
|
339
|
+
mkdirSync(dir, { recursive: true });
|
|
340
|
+
writeFileSync(file, `export const manifest = ${source};\n`);
|
|
341
|
+
try {
|
|
342
|
+
// The cache buster matters: `vp build` can run twice in one process.
|
|
343
|
+
return { manifest: (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).manifest, source };
|
|
344
|
+
} finally {
|
|
345
|
+
rmSync(file, { force: true });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The node table, in the positions the manifest's own route branches point at:
|
|
351
|
+
* for each node, the vite-root-relative path of its `+*.server.ts`, the
|
|
352
|
+
* component it renders, the client assets it needs and the page options it
|
|
353
|
+
* sets. Kit records the server path as `server_id` in the node modules it
|
|
354
|
+
* builds, and that is the only place the mapping from a branch slot back to an
|
|
355
|
+
* authored file survives the build.
|
|
356
|
+
*
|
|
357
|
+
* The positions are the trap. Kit *renumbers* nodes when it writes a manifest:
|
|
358
|
+
* `generate_manifest` collects the nodes the surviving routes use and hands each
|
|
359
|
+
* one a fresh consecutive index, so a route's `leaf` is a position in the
|
|
360
|
+
* manifest's node array and not the number the node's own module declares. The
|
|
361
|
+
* two agree only while every node is used — and prerendering a single page drops
|
|
362
|
+
* that page's node and shifts every later one down by one.
|
|
363
|
+
*
|
|
364
|
+
* It shows up as a page whose Go load never runs while the layout above it works
|
|
365
|
+
* perfectly, which reads like a bug in the load rather than in the manifest.
|
|
366
|
+
*
|
|
367
|
+
* The renumbering is recoverable because kit writes the original index into each
|
|
368
|
+
* node's import path (`__memo(() => import('./nodes/6.js'))`), in the new order.
|
|
369
|
+
*
|
|
370
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
371
|
+
* @param {string} source kit's generated manifest, as text
|
|
372
|
+
* @param {any} kit the same manifest, imported
|
|
373
|
+
* @returns {Promise<Node[]>}
|
|
374
|
+
*/
|
|
375
|
+
async function readNodes(builder, source, kit) {
|
|
376
|
+
const dir = join(builder.getServerDirectory(), 'nodes');
|
|
377
|
+
/** @type {Map<number, string>} */
|
|
378
|
+
const files = new Map();
|
|
379
|
+
for (const name of readdirSync(dir)) {
|
|
380
|
+
if (!name.endsWith('.js')) continue;
|
|
381
|
+
const file = join(dir, name);
|
|
382
|
+
const index = Number(readFileSync(file, 'utf-8').match(/^export const index = (\d+);$/m)?.[1]);
|
|
383
|
+
if (!Number.isInteger(index)) {
|
|
384
|
+
throw new Error(`skgo: ${file} does not declare a node index`);
|
|
385
|
+
}
|
|
386
|
+
files.set(index, file);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const order = [...source.matchAll(/import\('[^']*\/nodes\/(\d+)\.js'\)/g)].map((m) =>
|
|
390
|
+
Number(m[1])
|
|
391
|
+
);
|
|
392
|
+
if (order.length !== kit._.nodes.length) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`skgo: kit's manifest holds ${kit._.nodes.length} node(s) but ${order.length} node import(s) could be read out of it. ` +
|
|
395
|
+
'The manifest no longer says which node module each branch slot points at, and skgo would silently run the wrong load.'
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** @type {Node[]} */
|
|
400
|
+
const nodes = [];
|
|
401
|
+
for (const index of order) {
|
|
402
|
+
const file = files.get(index);
|
|
403
|
+
if (!file) {
|
|
404
|
+
throw new Error(`skgo: kit's manifest imports nodes/${index}.js, which the build did not write`);
|
|
405
|
+
}
|
|
406
|
+
const module = await import(pathToFileURL(file).href);
|
|
407
|
+
|
|
408
|
+
if (typeof module.universal?.load === 'function') {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`skgo: ${module.universal_id} exports a \`load\`. Every load in a skgo app is written in Go; ` +
|
|
411
|
+
'a universal load would have to run in the browser and in the SSR engine, and skgo runs neither.'
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
nodes.push({
|
|
416
|
+
index,
|
|
417
|
+
server: module.server_id ?? '',
|
|
418
|
+
component: module.component ? componentSource(file, dir) : null,
|
|
419
|
+
imports: module.imports ?? [],
|
|
420
|
+
stylesheets: module.stylesheets ?? [],
|
|
421
|
+
fonts: module.fonts ?? [],
|
|
422
|
+
ssr: option(module, 'ssr'),
|
|
423
|
+
csr: option(module, 'csr')
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return nodes;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @typedef {{
|
|
432
|
+
* index: number,
|
|
433
|
+
* server: string,
|
|
434
|
+
* component: string | null,
|
|
435
|
+
* imports: string[],
|
|
436
|
+
* stylesheets: string[],
|
|
437
|
+
* fonts: Array<{ file: string, filename: string }>,
|
|
438
|
+
* ssr: boolean | null,
|
|
439
|
+
* csr: boolean | null
|
|
440
|
+
* }} Node
|
|
441
|
+
*/
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* One of the two page options a node can set, from wherever it set it. Kit
|
|
445
|
+
* reads the universal module first and falls back to the server one
|
|
446
|
+
* (packages/kit/src/utils/page_nodes.js); Go does the reduction over the branch.
|
|
447
|
+
*
|
|
448
|
+
* @param {any} module
|
|
449
|
+
* @param {'ssr' | 'csr'} option
|
|
450
|
+
*/
|
|
451
|
+
function option(module, option) {
|
|
452
|
+
return module.universal?.[option] ?? module.server?.[option] ?? null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* The `.svelte` file a node's component was compiled from, vite-root-relative.
|
|
457
|
+
*
|
|
458
|
+
* It comes out of the sourcemap kit's own build wrote beside the compiled
|
|
459
|
+
* entry, because that is the record of what was compiled. Reversing kit's
|
|
460
|
+
* `[id]` -> `_id_` directory encoding by hand is a guess that breaks on
|
|
461
|
+
* `[...rest]`, and a wrong guess here renders the wrong page.
|
|
462
|
+
*
|
|
463
|
+
* @param {string} file the node module
|
|
464
|
+
* @param {string} dir the directory it lives in
|
|
465
|
+
*/
|
|
466
|
+
function componentSource(file, dir) {
|
|
467
|
+
const entry = readFileSync(file, 'utf-8').match(/import\('(\.\.\/entries\/[^']+)'\)/)?.[1];
|
|
468
|
+
if (!entry) {
|
|
469
|
+
throw new Error(`skgo: ${file} declares a component but does not import one`);
|
|
470
|
+
}
|
|
471
|
+
const map = resolve(dir, entry) + '.map';
|
|
472
|
+
const sources = JSON.parse(readFileSync(map, 'utf-8')).sources;
|
|
473
|
+
const own = resolve(dirname(map), sources[sources.length - 1]);
|
|
474
|
+
const name = basename(own);
|
|
475
|
+
const authored = /^\+(page|layout|error)\.svelte$/.test(name);
|
|
476
|
+
const fallback = /^(layout|error)\.svelte$/.test(name) && own === join(KIT_COMPONENTS, name);
|
|
477
|
+
if (!authored && !fallback) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`skgo: ${map} names ${own} as the last source of ${entry}, which is not a page, layout or error component. ` +
|
|
480
|
+
'The sourcemap no longer says which component each node renders.'
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return relative(process.cwd(), own);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Everything the Go process needs to render a document that is not already in
|
|
488
|
+
* the manifest: where the bundle and the template are, the global the boot
|
|
489
|
+
* script assigns, the client entry points and each node's client assets, and
|
|
490
|
+
* the `ssr`/`csr` options Go reduces over a route's branch.
|
|
491
|
+
*
|
|
492
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
493
|
+
* @param {any} kit kit's own server manifest
|
|
494
|
+
* @param {Node[]} nodes
|
|
495
|
+
*/
|
|
496
|
+
function describeSSR(builder, kit, nodes) {
|
|
497
|
+
const client = kit._.client;
|
|
498
|
+
if (!client) {
|
|
499
|
+
throw new Error('skgo: kit built no client bundle, so there is nothing for a document to boot.');
|
|
500
|
+
}
|
|
501
|
+
if (client.inline) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
"skgo: `output.bundleStrategy: 'inline'` is not supported. skgo assembles the boot script itself and only knows the split form."
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (client.routes) {
|
|
507
|
+
throw new Error(
|
|
508
|
+
"skgo: `router.resolution: 'server'` is not supported. skgo's routing is Go's, and the client resolves its own routes."
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
bundle: 'ssr/bundle.js',
|
|
514
|
+
template: 'app.html',
|
|
515
|
+
errorTemplate: 'error.html',
|
|
516
|
+
target: SSR_TARGET,
|
|
517
|
+
globalName: globalName(builder.config),
|
|
518
|
+
assets: builder.config.paths.assets,
|
|
519
|
+
relative: builder.config.paths.relative,
|
|
520
|
+
// kit's own validated `csp` option (`core/config/options.js`), copied
|
|
521
|
+
// verbatim: `mode`, `directives` and `reportOnly` are plain data, and
|
|
522
|
+
// Go assembles the document's CSP header and boot-script nonce/hash
|
|
523
|
+
// itself (`csp.go`) rather than running kit's `render.js`/`csp.js` in
|
|
524
|
+
// the engine. A directive kit's schema left `undefined` is dropped by
|
|
525
|
+
// this JSON.stringify, exactly as it would be from kit's own — so an
|
|
526
|
+
// app that never sets `csp` gets a build that describes no policy at
|
|
527
|
+
// all, and Go sets no header.
|
|
528
|
+
csp: builder.config.csp,
|
|
529
|
+
client: {
|
|
530
|
+
start: client.start,
|
|
531
|
+
app: client.app ?? '',
|
|
532
|
+
imports: client.imports ?? [],
|
|
533
|
+
stylesheets: client.stylesheets ?? [],
|
|
534
|
+
fonts: client.fonts ?? [],
|
|
535
|
+
usesEnvDynamicPublic: !!client.uses_env_dynamic_public
|
|
536
|
+
},
|
|
537
|
+
nodes: nodes.map((node) => ({
|
|
538
|
+
// The index the node's own module declares, which is *not* its
|
|
539
|
+
// position in this array: kit renumbers the nodes it puts in a
|
|
540
|
+
// manifest and keeps the original numbering in the client bundle.
|
|
541
|
+
// The boot script's `node_ids` are the original ones, and a
|
|
542
|
+
// document carrying the manifest's positions instead hydrates the
|
|
543
|
+
// wrong components — silently, because they are valid indices.
|
|
544
|
+
index: node.index,
|
|
545
|
+
component: !!node.component,
|
|
546
|
+
ssr: node.ssr,
|
|
547
|
+
csr: node.csr,
|
|
548
|
+
imports: node.imports,
|
|
549
|
+
stylesheets: node.stylesheets,
|
|
550
|
+
fonts: node.fonts
|
|
551
|
+
}))
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Kit's own manifest lists the remote *modules* it compiled, keyed by the hash
|
|
557
|
+
* it derived from each module's path. If that set is not the one `skgo
|
|
558
|
+
* generate` wrote, the two halves of the app were generated from different
|
|
559
|
+
* sources and the build must not succeed.
|
|
560
|
+
*
|
|
561
|
+
* Modules are as far as this check can reach. `generate_manifest` in kit's
|
|
562
|
+
* packages/kit/src/core/generate_manifest/index.js emits
|
|
563
|
+
* `'<hash>': __memo(() => import('./chunks/remote-<hash>.js'))` and nothing
|
|
564
|
+
* else; kit resolves the function half of an id at *runtime*, by looking the
|
|
565
|
+
* name up on the imported module namespace
|
|
566
|
+
* (packages/kit/src/runtime/server/remote-functions.js). There is no
|
|
567
|
+
* per-function list in the manifest to compare against. checkRemoteIds below
|
|
568
|
+
* reads the other build artifact that does carry the names.
|
|
569
|
+
*
|
|
570
|
+
* @param {any} kit kit's own server manifest
|
|
571
|
+
* @param {string[]} remotes
|
|
572
|
+
* @returns {Set<string>} the module hashes kit compiled
|
|
573
|
+
*/
|
|
574
|
+
function checkRemoteHashes(kit, remotes) {
|
|
575
|
+
if (!kit._ || typeof kit._.remotes !== 'object' || kit._.remotes === null) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
"skgo: kit's generated manifest has no `remotes` map. The adapter cannot tell which remote modules were compiled, so it cannot check them; this build of SvelteKit is not one skgo has been taught to read."
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
const built = new Set(Object.keys(kit._.remotes));
|
|
581
|
+
const declared = new Set(remotes.map((id) => id.split('/')[0]));
|
|
582
|
+
|
|
583
|
+
const missing = [...declared].filter((hash) => !built.has(hash));
|
|
584
|
+
const extra = [...built].filter((hash) => !declared.has(hash));
|
|
585
|
+
if (missing.length || extra.length) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
'skgo: skgo.remotes.json does not describe the remote modules kit just compiled.\n' +
|
|
588
|
+
(missing.length ? ` generated but not compiled: ${missing.join(', ')}\n` : '') +
|
|
589
|
+
(extra.length ? ` compiled but not generated: ${extra.join(', ')}\n` : '') +
|
|
590
|
+
' A generated .remote.ts only reaches the build once app code imports it. Run `go generate ./...`.'
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
return built;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Checks the remote functions the shipped bundle can actually call.
|
|
598
|
+
*
|
|
599
|
+
* The module-level check above is blind to an extra export added by hand to an
|
|
600
|
+
* already-generated `.remote.ts`: the module's hash does not change, so both
|
|
601
|
+
* sides still agree, the bundle calls `<hash>/<newName>`, and the Go binary —
|
|
602
|
+
* which was never told about it — answers 404 in the browser.
|
|
603
|
+
*
|
|
604
|
+
* Kit's client carries the full id as a literal. Its vite plugin rewrites every
|
|
605
|
+
* remote module's client half into one `export const <name> =
|
|
606
|
+
* __remote.<type>('<hash>/<name>')` per export
|
|
607
|
+
* (packages/kit/src/exports/vite/index.js), and the client runtime pastes that
|
|
608
|
+
* id straight into `${base}/${app_dir}/remote/${id}`
|
|
609
|
+
* (packages/kit/src/runtime/client/remote-functions/query/index.js). So the ids
|
|
610
|
+
* survive bundling and minification as string literals, and the set of them in
|
|
611
|
+
* the client we are about to ship is exactly the set of remote calls the
|
|
612
|
+
* browser can make.
|
|
613
|
+
*
|
|
614
|
+
* Every one of those must be a function `skgo generate` declared. The reverse
|
|
615
|
+
* is not required: a declared function no component imports is treeshaken out
|
|
616
|
+
* of the client, and Go serving a function nobody calls yet breaks nothing.
|
|
617
|
+
*
|
|
618
|
+
* @param {string} clientDir the client bundle this build will ship
|
|
619
|
+
* @param {string[]} remotes
|
|
620
|
+
* @param {Set<string>} hashes the module hashes kit compiled
|
|
621
|
+
*/
|
|
622
|
+
function checkRemoteIds(clientDir, remotes, hashes) {
|
|
623
|
+
const declared = new Set(remotes);
|
|
624
|
+
const called = new Map();
|
|
625
|
+
|
|
626
|
+
for (const file of walk(clientDir)) {
|
|
627
|
+
if (!file.endsWith('.js')) continue;
|
|
628
|
+
const source = readFileSync(file, 'utf-8');
|
|
629
|
+
for (const [, hash, name] of source.matchAll(
|
|
630
|
+
/['"`]([A-Za-z0-9]{1,16})\/([A-Za-z_$][A-Za-z0-9_$]*)['"`]/g
|
|
631
|
+
)) {
|
|
632
|
+
if (!hashes.has(hash)) continue;
|
|
633
|
+
called.set(`${hash}/${name}`, file);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (called.size === 0 && hashes.size > 0) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`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.`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const undeclared = [...called].filter(([id]) => !declared.has(id));
|
|
644
|
+
if (undeclared.length) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
'skgo: the built frontend calls remote functions that `skgo generate` did not write.\n' +
|
|
647
|
+
undeclared.map(([id, file]) => ` ${id} (in ${file})`).join('\n') +
|
|
648
|
+
'\n Go answers only the generated ids, so every one of these would 404 in the browser.\n' +
|
|
649
|
+
' A remote function written by hand in a .remote.ts is not a supported mode: write it in the matching .remote.go and run `go generate ./...`.'
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* @param {string} dir
|
|
656
|
+
* @returns {Generator<string>}
|
|
657
|
+
*/
|
|
658
|
+
function* walk(dir) {
|
|
659
|
+
for (const entry of readdirSync(dir)) {
|
|
660
|
+
const path = join(dir, entry);
|
|
661
|
+
if (statSync(path).isDirectory()) yield* walk(path);
|
|
662
|
+
else yield path;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The same check for server loads. Kit decides whether its client asks for
|
|
668
|
+
* `__data.json` at all by looking for a `load` export in the *built*
|
|
669
|
+
* `+*.server.js`, so a stub that did not reach the build is a page whose Go load
|
|
670
|
+
* is never called, and a compiled stub with no Go load behind it is a page whose
|
|
671
|
+
* data nobody answers.
|
|
672
|
+
*
|
|
673
|
+
* @param {string[]} nodes
|
|
674
|
+
* @param {string[]} loads
|
|
675
|
+
*/
|
|
676
|
+
function checkServerLoads(nodes, loads) {
|
|
677
|
+
const built = new Set(nodes.filter(Boolean));
|
|
678
|
+
const declared = new Set(loads);
|
|
679
|
+
|
|
680
|
+
const missing = [...declared].filter((id) => !built.has(id));
|
|
681
|
+
const extra = [...built].filter((id) => !declared.has(id));
|
|
682
|
+
if (missing.length === 0 && extra.length === 0) return;
|
|
683
|
+
|
|
684
|
+
throw new Error(
|
|
685
|
+
'skgo: skgo.remotes.json does not describe the server loads kit just compiled.\n' +
|
|
686
|
+
(missing.length ? ` generated but not compiled: ${missing.join(', ')}\n` : '') +
|
|
687
|
+
(extra.length ? ` compiled but not generated: ${extra.join(', ')}\n` : '') +
|
|
688
|
+
' Every server load is written in Go. Run `go generate ./...`.'
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Kit's `error.html`: the document a request gets when even the error page
|
|
694
|
+
* cannot be rendered. Kit reads the app's own `src/error.html` when it exists
|
|
695
|
+
* and its bundled default otherwise (`core/config/index.js`,
|
|
696
|
+
* `load_error_page`), and Go substitutes `%sveltekit.status%` and
|
|
697
|
+
* `%sveltekit.error.message%` into whichever one this wrote.
|
|
698
|
+
*
|
|
699
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
700
|
+
* @returns {string}
|
|
701
|
+
*/
|
|
702
|
+
function readErrorTemplate(builder) {
|
|
703
|
+
const file = builder.config.files.errorTemplate;
|
|
704
|
+
try {
|
|
705
|
+
return readFileSync(file, 'utf-8');
|
|
706
|
+
} catch {
|
|
707
|
+
// realpath for the same reason the SSR bundle needs it: pnpm links the
|
|
708
|
+
// package, and the default lives inside it.
|
|
709
|
+
const kit = realpathSync(join(process.cwd(), 'node_modules/@sveltejs/kit'));
|
|
710
|
+
return readFileSync(join(kit, 'src/core/config/default-error.html'), 'utf-8');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* @param {string} file
|
|
716
|
+
* @param {string} contents
|
|
717
|
+
*/
|
|
718
|
+
function write(file, contents) {
|
|
719
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
720
|
+
writeFileSync(file, contents);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* The module the SSR bundle reads the app's `transport` hook out of.
|
|
725
|
+
*
|
|
726
|
+
* kit's universal hooks file is the one declaration of a custom type's two
|
|
727
|
+
* halves that both sides share (`packages/kit/src/core/sync/write_server.js`
|
|
728
|
+
* loads the same file for the server, `write_client_manifest.js` for the
|
|
729
|
+
* client), so the engine loading anything else would be a third answer to a
|
|
730
|
+
* question that has two.
|
|
731
|
+
*
|
|
732
|
+
* A namespace import rather than a re-export: an app with a hooks file that
|
|
733
|
+
* declares only `reroute` must still build.
|
|
734
|
+
*
|
|
735
|
+
* @param {import('@sveltejs/kit').Builder} builder
|
|
736
|
+
*/
|
|
737
|
+
function universalHooks(builder) {
|
|
738
|
+
const file = resolveEntry(builder.config.files.hooks.universal);
|
|
739
|
+
if (!file) return 'export const transport = {};';
|
|
740
|
+
return (
|
|
741
|
+
`import * as hooks from ${JSON.stringify(file)};\n` +
|
|
742
|
+
'export const transport = hooks.transport ?? {};\n'
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* kit's `resolve_entry` (packages/kit/src/utils/filesystem.js): an
|
|
748
|
+
* extensionless path becomes the file that is actually there.
|
|
749
|
+
*
|
|
750
|
+
* @param {string} entry
|
|
751
|
+
* @returns {string | null}
|
|
752
|
+
*/
|
|
753
|
+
function resolveEntry(entry) {
|
|
754
|
+
if (existsSync(entry)) {
|
|
755
|
+
if (statSync(entry).isFile()) return entry;
|
|
756
|
+
const index = join(entry, 'index');
|
|
757
|
+
if (existsSync(index + '.js') || existsSync(index + '.ts')) return resolveEntry(index);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const dir = dirname(entry);
|
|
761
|
+
if (existsSync(dir)) {
|
|
762
|
+
const base = basename(entry);
|
|
763
|
+
const found = readdirSync(dir).find(
|
|
764
|
+
(file) => file.replace(/\.(js|ts)$/, '') === base && statSync(join(dir, file)).isFile()
|
|
765
|
+
);
|
|
766
|
+
if (found) return join(dir, found);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* The `globalThis.__sveltekit_xxx` name the boot script assigns and the client
|
|
774
|
+
* reads, derived the way kit derives it (packages/kit/src/core/utils.js).
|
|
775
|
+
*
|
|
776
|
+
* @param {any} config
|
|
777
|
+
*/
|
|
778
|
+
function globalName(config) {
|
|
779
|
+
return `__sveltekit_${djb2(config.version.name)}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* kit's djb2, packages/kit/src/utils/hash.js
|
|
784
|
+
* @param {string[]} values
|
|
785
|
+
*/
|
|
786
|
+
function djb2(...values) {
|
|
787
|
+
let hash = 5381;
|
|
788
|
+
for (const value of values) {
|
|
789
|
+
let i = value.length;
|
|
790
|
+
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
|
|
791
|
+
}
|
|
792
|
+
return (hash >>> 0).toString(36);
|
|
793
|
+
}
|