@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,420 @@
1
+ // The entry point. It mirrors the one region of kit's `render_response` that
2
+ // executes code (packages/kit/src/runtime/server/page/render.js): build the
3
+ // `Props` linked list, call `render(Root, ...)` inside kit's request store, and
4
+ // hand back `{ head, body }`. Everything on either side of that call — the boot
5
+ // script, the hydration array, the remote data, the head buckets, the template
6
+ // — is string assembly Go does with data Go already has.
7
+ //
8
+ // It is the `goja` environment's input, so kit's own build compiles what it
9
+ // reaches: the app's components through vite-plugin-svelte, its `.remote.ts`
10
+ // modules through kit's remote plugin, its TypeScript through vite. The
11
+ // `skgo:` specifiers are the kit internals `$app/*` does not name; the
12
+ // adapter's plugin resolves them to files inside kit itself.
13
+ import { render } from 'svelte/server';
14
+ import Root from 'skgo:kit/root';
15
+ import { Props, RenderNode } from 'skgo:kit/props';
16
+ import {
17
+ with_request_store,
18
+ HttpError,
19
+ Redirect,
20
+ SvelteKitError,
21
+ ValidationError
22
+ } from '@sveltejs/kit/internal/server';
23
+ import * as devalue from 'skgo:devalue';
24
+ import { decoders, init_transport, parse } from 'skgo:kit/transport';
25
+ import { components } from 'skgo:nodes';
26
+ import { transport } from 'skgo:hooks';
27
+
28
+ /**
29
+ * The app's transport hook, installed the way kit installs it
30
+ * (runtime/server/index.js: init_transport(module.transport ?? {})).
31
+ *
32
+ * Kit does it per request because it loads the hooks with a dynamic import;
33
+ * the hooks are static and this bundle has no top-level await, so it happens
34
+ * once per runtime instead. From here on, parse() is the app's own decoders
35
+ * and a value Go serialized under a transport key comes back as an instance
36
+ * of the class src/hooks.ts declares.
37
+ */
38
+ init_transport(transport ?? {});
39
+
40
+ /**
41
+ * kit's handle_error_and_jsonify (runtime/server/errors.js) with no
42
+ * handleError hook: an HttpError keeps the body the app chose, a framework
43
+ * error keeps its status and text, a validation failure is a Bad Request, and
44
+ * anything else is Internal Error with the real cause left on the server.
45
+ * skgo has no handleError hook, so there is nothing here to await or merge.
46
+ */
47
+ function handle_error(error) {
48
+ if (error instanceof HttpError) return error.body;
49
+ if (error instanceof SvelteKitError) return { status: error.status, message: error.text };
50
+ if (error instanceof ValidationError) return { status: 400, message: 'Bad Request' };
51
+ return { status: 500, message: 'Internal Error' };
52
+ }
53
+
54
+ /**
55
+ * A RequestState (packages/kit/src/types/internal.d.ts). Only the fields
56
+ * kit's remote wrappers read during a render are populated. is_in_render is
57
+ * what makes kit refuse a command.
58
+ */
59
+ function make_state() {
60
+ return {
61
+ getClientAddress: () => '127.0.0.1',
62
+ error: false,
63
+ rerouted_url: null,
64
+ depth: 0,
65
+ remote: {
66
+ data: null,
67
+ implicit: null,
68
+ explicit: null,
69
+ forms: null,
70
+ requested: null,
71
+ batches: null,
72
+ live_iterators: null
73
+ },
74
+ is_in_remote_function: false,
75
+ is_in_render: true
76
+ };
77
+ }
78
+
79
+ /**
80
+ * event.fetch during a render. Kit's own (runtime/server/fetch.js) resolves a
81
+ * relative URL against the page's own origin, forwards the incoming cookies
82
+ * on a same-origin request unless credentials is "omit", and otherwise hands
83
+ * the request to a real fetch. This engine has no socket to hand a
84
+ * cross-origin request to — no application I/O executes in JavaScript here —
85
+ * so a same-origin URL is a call back into Go's own handler for it, exactly
86
+ * the shape a remote function's call is, and a cross-origin one is refused: a
87
+ * thrown TypeError, before anything is dispatched. (Kit's own version also
88
+ * forwards the page's own Authorization header on a same-origin request; this
89
+ * one does not, because the render's own request never reaches the engine
90
+ * with its headers — only its cookies, in req.cookies.)
91
+ *
92
+ * @param {any} req
93
+ * @param {URL} url
94
+ */
95
+ function create_fetch(req, url) {
96
+ return async function (input, init) {
97
+ const request = input instanceof Request ? input : new Request(input, init);
98
+ const target = new URL(request.url, url);
99
+
100
+ if (!same_origin(target, url)) {
101
+ throw new TypeError(
102
+ "skgo: event.fetch only reaches this app's own routes during server-side rendering (" +
103
+ origin_of(target) +
104
+ ' is not ' +
105
+ origin_of(url) +
106
+ ')'
107
+ );
108
+ }
109
+
110
+ const credentials = init?.credentials ?? request.credentials ?? 'same-origin';
111
+ if (credentials !== 'omit') {
112
+ const cookie = Object.entries(req.cookies ?? {})
113
+ .map(([name, value]) => name + '=' + value)
114
+ .join('; ');
115
+ if (cookie) request.headers.set('cookie', cookie);
116
+ }
117
+
118
+ const headers = {};
119
+ request.headers.forEach((value, name) => {
120
+ headers[name] = value;
121
+ });
122
+
123
+ const envelope = { method: request.method || 'GET', url: target.href, headers };
124
+ if (typeof request._body === 'string') envelope.body = request._body;
125
+
126
+ const raw = await globalThis.__skgo_fetch(JSON.stringify(envelope));
127
+ const answer = JSON.parse(raw);
128
+ if (answer.error) throw new TypeError(answer.error);
129
+
130
+ return Promise.resolve(
131
+ new Response(answer.response.body ?? '', {
132
+ status: answer.response.status,
133
+ headers: answer.response.headers
134
+ })
135
+ );
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Same-origin, spelled out rather than taken from `origin`. The engine's URL is
141
+ * Go's (goja_nodejs, over net/url) and its `origin` is scheme and hostname with
142
+ * the port left out, which would make a page on :8080 and a URL on :9999 look
143
+ * like the same site — and this is the one comparison standing between a
144
+ * cross-origin fetch and Go's own handler for the path. Protocol, hostname and
145
+ * port are each exactly what the standard compares.
146
+ *
147
+ * @param {URL} a
148
+ * @param {URL} b
149
+ */
150
+ function same_origin(a, b) {
151
+ return a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
152
+ }
153
+
154
+ /**
155
+ * What `url.origin` would say if this engine's URL reported it the way the
156
+ * standard does. Only the refusal message above needs it.
157
+ *
158
+ * @param {URL} url
159
+ */
160
+ function origin_of(url) {
161
+ return url.host ? url.protocol + '//' + url.host : 'null';
162
+ }
163
+
164
+ /**
165
+ * A RequestEvent stand-in. run_remote_function spreads it and derives the
166
+ * event a query actually sees, which is where kit makes url, params and
167
+ * route throw. Go owns the real request; the only I/O anything here performs
168
+ * is `fetch`'s in-process call back into Go for one of the app's own routes.
169
+ */
170
+ function make_event(req, url) {
171
+ return {
172
+ cookies: {
173
+ get: (name) => (req.cookies ?? {})[name],
174
+ getAll: () => Object.entries(req.cookies ?? {}).map(([name, value]) => ({ name, value })),
175
+ set: () => {},
176
+ delete: () => {},
177
+ serialize: () => ''
178
+ },
179
+ fetch: create_fetch(req, url),
180
+ getClientAddress: () => req.client_address ?? '127.0.0.1',
181
+ locals: {},
182
+ params: req.params ?? {},
183
+ platform: undefined,
184
+ request: { headers: { get: () => null }, method: 'GET' },
185
+ route: { id: req.route_id ?? null },
186
+ setHeaders: () => {},
187
+ url,
188
+ isDataRequest: false,
189
+ isSubRequest: false,
190
+ isRemoteRequest: false,
191
+ tracing: { enabled: false }
192
+ };
193
+ }
194
+
195
+ /**
196
+ * One node's load result. Go sends devalue's flat form — the same bytes it
197
+ * sends the client in __data.json, produced by the same encoders — and the
198
+ * app's decoders read it back, so the component renders against the instance
199
+ * the browser is about to hold rather than the object its fields travelled in.
200
+ *
201
+ * A value the load promised is in those bytes as kit's own placeholder, and it
202
+ * is read back the way kit's client reads it (process_stream in client.js): a
203
+ * Promise reviver alongside the app's decoders. So a promise is found wherever
204
+ * the load left one, at any depth, which is where devalue's reducer put it.
205
+ *
206
+ * The promise it becomes never settles. Svelte's server renderer does not await
207
+ * an await block — it pushes the block marker and renders the pending branch
208
+ * (svelte/src/internal/server/index.js, await_block) — so the document leaves Go
209
+ * with the loading state already in it, and the value follows it down as a chunk
210
+ * Go appends. That is exactly what kit does, which hands its renderer the
211
+ * promise itself.
212
+ */
213
+ function node_data(node) {
214
+ if (!node.data) return null;
215
+ return devalue.parse(node.data, {
216
+ ...decoders,
217
+ Promise: () => new Promise(() => {})
218
+ });
219
+ }
220
+
221
+ function build_props(req, url) {
222
+ const page = {
223
+ error: req.error ?? null,
224
+ params: req.params ?? {},
225
+ route: { id: req.route_id ?? null },
226
+ status: req.status ?? 200,
227
+ url,
228
+ data: {},
229
+ form: req.form ?? null,
230
+ shallow: null,
231
+ state: {}
232
+ };
233
+
234
+ const branch = req.branch ?? [];
235
+ const error_components = (req.error_components ?? []).map((i) =>
236
+ i == null ? undefined : components[i]
237
+ );
238
+
239
+ const props = new Props({
240
+ page,
241
+ tree: new RenderNode(components[branch[0].node], undefined),
242
+ form: req.form ?? null,
243
+ error: req.error ?? undefined
244
+ });
245
+
246
+ let current_node = props.tree;
247
+ let data = props.page.data;
248
+
249
+ for (let i = 0; i < branch.length; i += 1) {
250
+ data = { ...data, ...node_data(branch[i]) };
251
+ current_node.data = data;
252
+
253
+ if (i < branch.length - 1) {
254
+ current_node = current_node.child = new RenderNode(
255
+ components[branch[i + 1].node],
256
+ error_components[i + 1]
257
+ );
258
+ }
259
+ }
260
+
261
+ props.page.data = data;
262
+ return props;
263
+ }
264
+
265
+ /**
266
+ * Puts a non-enhanced submission's outcome where kit's form instance reads
267
+ * it: the request's remote cache, under the instance's own internals object
268
+ * and the empty-string key.
269
+ *
270
+ * That is the last thing kit's form wrapper does after it runs a submission
271
+ * (runtime/app/server/remote/form.js: get_cache(__, state)[''] ??= output),
272
+ * and it is what makes myForm.result, myForm.fields.x.issues() and the value
273
+ * of every control render the submission. Nothing here runs a form body — Go
274
+ * already ran it — so the stubs still throw and a rendered result is still
275
+ * proof that Go answered.
276
+ *
277
+ * The output arrives in devalue's flat form and is read back with the app's
278
+ * own decoders, for the same reason a load's data is: a result carrying a
279
+ * transported type has to reach the component as an instance of its class.
280
+ *
281
+ * A keyed instance, one created by calling for(key) on a form, needs one more
282
+ * step than an unkeyed one. Go's req.form_action.id is kit's own composite
283
+ * id: the base hash/name, or that plus a slash and the key's JSON text — the
284
+ * same string kit's server files a form's output under in the page's remote
285
+ * data — and only the base half is registered anywhere: __skgo_forms holds
286
+ * the module-level instance kit's form factory created, with no key at all.
287
+ * That instance's own for method is kit's own code
288
+ * (runtime/app/server/remote/form.js) for turning a key into the actual
289
+ * per-key instance the page's own call to for(key) will return — it caches
290
+ * what it creates in the request's form cache, keyed by the base id and the
291
+ * key's JSON text together, so calling it here, before the page component
292
+ * runs, makes the page's later call resolve to the very instance seeded below
293
+ * rather than a fresh, empty one. Calling for reaches into the request
294
+ * store, which is why this function now has to run inside with_request_store
295
+ * rather than before it.
296
+ */
297
+ function seed_form(req, state) {
298
+ const seed = req.form_action;
299
+ if (!seed) return;
300
+
301
+ const first_slash = seed.id.indexOf('/');
302
+ const second_slash = seed.id.indexOf('/', first_slash + 1);
303
+ const base_id = second_slash === -1 ? seed.id : seed.id.slice(0, second_slash);
304
+ const key_json = second_slash === -1 ? undefined : seed.id.slice(second_slash + 1);
305
+
306
+ for (const instance of globalThis.__skgo_forms ?? []) {
307
+ if (!instance.__ || instance.__.id !== base_id) continue;
308
+
309
+ const target = key_json === undefined ? instance : instance.for(JSON.parse(key_json));
310
+ (state.remote.data ??= new Map()).set(target.__, { '': parse(seed.output) });
311
+ return;
312
+ }
313
+
314
+ throw new Error('skgo: no form is registered as ' + seed.id);
315
+ }
316
+
317
+ /**
318
+ * Renders one page. The result object is filled in as the promise chain
319
+ * settles; the host drains the job queue when this call returns, so done is
320
+ * true by then or the render never finished — which is a Go error, not a
321
+ * partial document.
322
+ */
323
+ globalThis.__skgo_render = function (req_json) {
324
+ const result = {
325
+ done: false,
326
+ // The message of a render that threw with nothing to catch it. Go turns
327
+ // it into a Go error and answers the request some other way; it is never
328
+ // part of a document.
329
+ failure: '',
330
+ // A redirect thrown during the render — by a remote function, say. Kit
331
+ // answers the whole document with a bare 3xx (render_page's catch), and
332
+ // so does Go.
333
+ redirect: null,
334
+ // The status and error the document is answered with. They start as the
335
+ // ones Go asked for and are replaced by transformError if a boundary
336
+ // catches something, which is where kit sets them too.
337
+ status: 200,
338
+ error: null,
339
+ head: '',
340
+ body: ''
341
+ };
342
+
343
+ try {
344
+ const req = JSON.parse(req_json);
345
+ const url = new URL(req.url);
346
+ const props = build_props(req, url);
347
+ const state = make_state();
348
+ const event = make_event(req, url);
349
+
350
+ result.status = props.page.status;
351
+ result.error = req.error ?? null;
352
+
353
+ const options = {
354
+ context: new Map([['__request__', { page: props.page }]]),
355
+ // kit's own \`csp.script_needs_nonce ? { nonce: csp.nonce } : {
356
+ // hash: csp.script_needs_hash }\` (page/render.js:198), passed to
357
+ // Svelte's own renderer so the one inline script Svelte can still
358
+ // emit on its own — its hydratable-async-block script
359
+ // (internal/server/renderer.js's #hydratable_block, for a
360
+ // component's own top-level await) — carries the same nonce or
361
+ // hash decision the boot script gets from Go (csp.go). Go decided
362
+ // which branch this request is in before the engine ever ran;
363
+ // req.csp carries only the answer.
364
+ csp: req.csp.nonce ? { nonce: req.csp.nonce } : { hash: !!req.csp.hash },
365
+ // kit's own (page/render.js): the transform every error boundary's
366
+ // error passes through on its way to the failed snippet. It is
367
+ // what makes page.status and page.error inside a rendering
368
+ // component the values kit would give, and what turns an
369
+ // unexpected throw into Internal Error rather than a stack trace on
370
+ // the page. A redirect is rethrown, because a redirect is an answer
371
+ // for the whole document rather than for one boundary.
372
+ transformError: (e) => {
373
+ if (e instanceof Redirect) throw e;
374
+ const handled = handle_error(e);
375
+ result.error = handled;
376
+ result.status = handled.status;
377
+ props.page.error = handled;
378
+ props.page.status = handled.status;
379
+ return handled;
380
+ }
381
+ };
382
+ // seed_form runs inside the request store rather than before it: a
383
+ // keyed submission's call to for(key) needs the request store, the
384
+ // same as the page component's own call to it does.
385
+ const promise = with_request_store({ event, state }, () => {
386
+ seed_form(req, state);
387
+ return render(Root, { ...options, props });
388
+ });
389
+
390
+ Promise.resolve(promise).then(
391
+ (rendered) => {
392
+ result.head = rendered.head;
393
+ result.body = rendered.body;
394
+ result.done = true;
395
+ },
396
+ (err) => {
397
+ if (err instanceof Redirect) {
398
+ result.redirect = { status: err.status, location: err.location };
399
+ } else {
400
+ result.failure = (err && (err.stack || err.message)) || String(err);
401
+ }
402
+ result.done = true;
403
+ }
404
+ );
405
+ } catch (err) {
406
+ if (err instanceof Redirect) {
407
+ result.redirect = { status: err.status, location: err.location };
408
+ } else {
409
+ result.failure = (err && (err.stack || err.message)) || String(err);
410
+ }
411
+ result.done = true;
412
+ }
413
+
414
+ return result;
415
+ };
416
+
417
+ // A liveness check Go calls once, when it puts a fresh runtime into the pool.
418
+ globalThis.__skgo_ping = function () {
419
+ return 'ok';
420
+ };