@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.
@@ -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
+ }