@skgo/sveltekit-adapter 0.13.0 → 0.14.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skgo/sveltekit-adapter",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "SvelteKit adapter for skgo: builds a frontend that a Go binary renders and answers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -24,6 +24,7 @@ import * as devalue from 'skgo:devalue';
24
24
  import { decoders, init_transport, parse } from 'skgo:kit/transport';
25
25
  import { components, universalLoads } from 'skgo:nodes';
26
26
  import { transport } from 'skgo:hooks';
27
+ import { serialize_data } from 'skgo:kit/serialize-data';
27
28
 
28
29
  /**
29
30
  * The app's transport hook, installed the way kit installs it
@@ -76,89 +77,140 @@ function make_state() {
76
77
  };
77
78
  }
78
79
 
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
- */
80
+ /** All render-time I/O is dispatched by Go, including the handleFetch hook. */
95
81
  function create_fetch(req, url) {
96
82
  return async function (input, init) {
97
- const request = input instanceof Request ? input : new Request(input, init);
83
+ const request = input instanceof Request ? input : new Request(new URL(input, url).href, init);
98
84
  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
85
  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 };
86
+ request.headers.forEach((value, name) => { headers[name] = value; });
87
+ const envelope = {
88
+ method: request.method || 'GET', url: target.href, headers,
89
+ credentials: request.credentials, mode: request.mode
90
+ };
124
91
  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);
92
+ else if (request._body != null) throw new TypeError('skgo: render-time fetch request bodies must be text');
93
+ const answer = JSON.parse(await globalThis.__skgo_fetch(JSON.stringify(envelope)));
128
94
  if (answer.error) throw new TypeError(answer.error);
95
+ const value = answer.response;
96
+ const body = value.bodyBase64
97
+ ? Uint8Array.from(atob(value.bodyBase64), (c) => c.charCodeAt(0))
98
+ : value.body ?? '';
99
+ const response = new Response(body, { status: value.status, statusText: value.statusText, headers: value.headers });
100
+ response._serializedHeaders = value.serializedHeaders ?? {};
101
+ return response;
102
+ };
103
+ }
129
104
 
130
- return Promise.resolve(
131
- new Response(answer.response.body ?? '', {
132
- status: answer.response.status,
133
- headers: answer.response.headers
134
- })
135
- );
105
+ /**
106
+ * Kit's universal-load fetch contract (server/page/load_data.js): record bodies
107
+ * when consumed, hash the author's headers/body rather than inherited cookies,
108
+ * and reject access to headers hydration will not receive. The final HTML is
109
+ * emitted by Kit's own serialize_data, including escaping, hashes and TTLs.
110
+ */
111
+ function create_universal_fetch(event, req, fetched) {
112
+ return async (input, init) => {
113
+ const request_headers = input instanceof Request
114
+ ? ([...input.headers].length ? new Headers(input.headers) : undefined)
115
+ : init?.headers;
116
+ const request_body = input instanceof Request ? input._body : init?.body;
117
+ const url = new URL(input instanceof Request ? input.url : input, event.url);
118
+ let response = await event.fetch(input, init);
119
+ if (!same_origin(url, event.url) && (url.protocol === 'http:' || url.protocol === 'https:')) {
120
+ const mode = input instanceof Request ? input.mode : init?.mode ?? 'cors';
121
+ if (mode === 'no-cors') {
122
+ const allowed = response._serializedHeaders;
123
+ response = new Response('', { status: response.status, statusText: response.statusText, headers: response.headers });
124
+ response._serializedHeaders = allowed;
125
+ } else {
126
+ const acao = response.headers.get('access-control-allow-origin');
127
+ if (!acao || (acao !== '*' && acao !== origin_of(event.url))) {
128
+ throw new Error("CORS error: incorrect or missing 'Access-Control-Allow-Origin' header");
129
+ }
130
+ }
131
+ }
132
+ const cache_url = same_origin(url, event.url)
133
+ ? url.pathname + url.search
134
+ : url.href;
135
+ const allowed = response._serializedHeaders;
136
+ const wrap = (target) => {
137
+ const record = (body, is_b64) => {
138
+ fetched.push({
139
+ url: cache_url, method: event.request.method,
140
+ request_headers, request_body, response: target,
141
+ response_body: body, is_b64, allowed
142
+ });
143
+ };
144
+ const text = async () => {
145
+ const body = await target.text();
146
+ if (body === '' && [101, 204, 205, 304].includes(target.status)) {
147
+ record(undefined, false);
148
+ return undefined;
149
+ }
150
+ record(body, false);
151
+ return body;
152
+ };
153
+ const headers = new Headers(target.headers);
154
+ const get = headers.get.bind(headers);
155
+ if (req.csr) {
156
+ headers.get = (key) => {
157
+ const lower = key.toLowerCase();
158
+ const value = get(lower);
159
+ if (value && !lower.startsWith('x-sveltekit-') && !(lower in allowed)) {
160
+ throw new Error(`Failed to get response header "${lower}" — it must be included by the filterSerializedResponseHeaders option (at ${event.route.id})`);
161
+ }
162
+ return value;
163
+ };
164
+ }
165
+ return new Proxy(target, {
166
+ get(target, key) {
167
+ if (key === 'headers') return headers;
168
+ if (key === 'text') return text;
169
+ if (key === 'json') return async () => { const body = await text(); return body ? JSON.parse(body) : undefined; };
170
+ if (key === 'arrayBuffer') return async () => {
171
+ const buffer = await target.arrayBuffer();
172
+ let binary = '';
173
+ for (const byte of new Uint8Array(buffer)) binary += String.fromCharCode(byte);
174
+ record(btoa(binary), true);
175
+ return buffer;
176
+ };
177
+ if (key === 'clone') return () => wrap(target.clone());
178
+ const value = Reflect.get(target, key, target);
179
+ return typeof value === 'function' ? value.bind(target) : value;
180
+ }
181
+ });
182
+ };
183
+ return wrap(response);
136
184
  };
137
185
  }
138
186
 
187
+
139
188
  /**
140
189
  * Same-origin, spelled out rather than taken from `origin`. The engine's URL is
141
190
  * Go's (goja_nodejs, over net/url) and its `origin` is scheme and hostname with
142
191
  * the port left out, which would make a page on :8080 and a URL on :9999 look
143
192
  * 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.
193
+ * cached response and a foreign response. Protocol, hostname and
194
+ * port are each exactly what the standard compares. Go enforces dispatch separately.
146
195
  *
147
196
  * @param {URL} a
148
197
  * @param {URL} b
149
198
  */
150
199
  function same_origin(a, b) {
151
- return a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
200
+ const port = (url) => url.port || (url.protocol === 'https:' ? '443' : '80');
201
+ return a.protocol === b.protocol && a.hostname === b.hostname && port(a) === port(b);
152
202
  }
153
203
 
154
204
  /**
155
205
  * 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.
206
+ * standard does, used for CORS validation.
157
207
  *
158
208
  * @param {URL} url
159
209
  */
160
210
  function origin_of(url) {
161
- return url.host ? url.protocol + '//' + url.host : 'null';
211
+ const default_port = (url.protocol === 'http:' && url.port === '80') || (url.protocol === 'https:' && url.port === '443');
212
+ const host = default_port ? url.host.replace(/:(80|443)$/, '') : url.host;
213
+ return host ? url.protocol + '//' + host : 'null';
162
214
  }
163
215
 
164
216
  /**
@@ -181,7 +233,7 @@ function make_event(req, url) {
181
233
  locals: {},
182
234
  params: req.params ?? {},
183
235
  platform: undefined,
184
- request: { headers: { get: () => null }, method: 'GET' },
236
+ request: { headers: new Headers(req.headers), method: req.method ?? 'GET' },
185
237
  route: { id: req.route_id ?? null },
186
238
  setHeaders: () => {},
187
239
  url,
@@ -218,7 +270,7 @@ function node_data(node) {
218
270
  });
219
271
  }
220
272
 
221
- async function build_props(req, url, event, state) {
273
+ async function build_props(req, url, event, state, fetched) {
222
274
  const form = req.form ? devalue.parse(req.form, decoders) : null;
223
275
  const page = {
224
276
  error: req.error ?? null,
@@ -261,9 +313,7 @@ async function build_props(req, url, event, state) {
261
313
  parent: async () => parent_data,
262
314
  depends: () => {},
263
315
  untrack: (fn) => fn(),
264
- fetch: () => {
265
- throw new Error('skgo: universal load fetch is not supported during rendering');
266
- },
316
+ fetch: create_universal_fetch(event, req, fetched),
267
317
  setHeaders: () => {
268
318
  throw new Error('skgo: universal load setHeaders is not supported during rendering');
269
319
  }
@@ -363,7 +413,8 @@ globalThis.__skgo_render = function (req_json) {
363
413
  status: 200,
364
414
  error: null,
365
415
  head: '',
366
- body: ''
416
+ body: '',
417
+ fetched: ''
367
418
  };
368
419
 
369
420
  try {
@@ -371,7 +422,8 @@ globalThis.__skgo_render = function (req_json) {
371
422
  const url = new URL(req.url);
372
423
  const state = make_state();
373
424
  const event = make_event(req, url);
374
- const props_promise = build_props(req, url, event, state);
425
+ const fetched = [];
426
+ const props_promise = build_props(req, url, event, state, fetched);
375
427
  const promise = props_promise.then((props) => {
376
428
  result.status = props.page.status;
377
429
  result.error = req.error ?? null;
@@ -417,6 +469,7 @@ globalThis.__skgo_render = function (req_json) {
417
469
  (rendered) => {
418
470
  result.head = rendered.head;
419
471
  result.body = rendered.body;
472
+ result.fetched = fetched.map((item) => serialize_data(item, (key) => key in item.allowed)).join("\n\t\t\t");
420
473
  result.done = true;
421
474
  },
422
475
  (err) => {
@@ -132,6 +132,7 @@ function kitAliases(root) {
132
132
  'skgo:kit/remote': join(runtime, 'app/server/remote/index.js'),
133
133
  'skgo:kit/shared': join(runtime, 'shared.js'),
134
134
  'skgo:kit/transport': join(runtime, 'app/internal/transport.js'),
135
+ 'skgo:kit/serialize-data': join(runtime, 'server/page/serialize_data.js'),
135
136
  'skgo:kit/props': join(runtime, 'props.svelte.js'),
136
137
  'skgo:kit/root': join(runtime, 'components/root.svelte'),
137
138
  // kit's own `$app/paths` server implementation, which app-paths.js
@@ -39,9 +39,9 @@ if (typeof globalThis.Headers === 'undefined') {
39
39
  if (init instanceof Headers) {
40
40
  for (const [k, v] of init._) this._.set(k, v);
41
41
  } else if (Array.isArray(init)) {
42
- for (const [k, v] of init) this.set(k, v);
42
+ for (const [k, v] of init) this.append(k, v);
43
43
  } else if (init && typeof init === 'object') {
44
- for (const k of Object.keys(init)) this.set(k, init[k]);
44
+ for (const k of Object.keys(init)) this.append(k, init[k]);
45
45
  }
46
46
  }
47
47
  set(name, value) {
@@ -66,19 +66,19 @@ if (typeof globalThis.Headers === 'undefined') {
66
66
  this._.delete(String(name).toLowerCase());
67
67
  }
68
68
  forEach(fn, thisArg) {
69
- for (const [k, v] of this._) fn.call(thisArg, v, k, this);
69
+ for (const [k, v] of this.entries()) fn.call(thisArg, v, k, this);
70
70
  }
71
71
  keys() {
72
- return this._.keys();
72
+ return Array.from(this.entries(), ([k]) => k)[Symbol.iterator]();
73
73
  }
74
74
  values() {
75
- return this._.values();
75
+ return Array.from(this.entries(), ([, v]) => v)[Symbol.iterator]();
76
76
  }
77
77
  entries() {
78
- return this._.entries();
78
+ return Array.from(this._).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)[Symbol.iterator]();
79
79
  }
80
80
  [Symbol.iterator]() {
81
- return this._.entries();
81
+ return this.entries();
82
82
  }
83
83
  };
84
84
  }
@@ -111,6 +111,7 @@ if (typeof globalThis.Request === 'undefined') {
111
111
  }
112
112
  async text() { return this._body ?? ''; }
113
113
  async json() { return JSON.parse(this._body ?? 'null'); }
114
+ clone() { return new Request(this); }
114
115
  };
115
116
  }
116
117
 
@@ -123,11 +124,11 @@ if (typeof globalThis.Response === 'undefined') {
123
124
  this.headers = init.headers instanceof Headers ? init.headers : new Headers(init.headers);
124
125
  this.ok = this.status >= 200 && this.status < 300;
125
126
  }
126
- async text() { return this._body; }
127
- async json() { return JSON.parse(this._body); }
128
- async arrayBuffer() { return new TextEncoder().encode(this._body).buffer; }
127
+ async text() { return typeof this._body === 'string' ? this._body : new TextDecoder().decode(this._body); }
128
+ async json() { return JSON.parse(await this.text()); }
129
+ async arrayBuffer() { return (typeof this._body === 'string' ? new TextEncoder().encode(this._body) : new Uint8Array(this._body)).buffer; }
129
130
  clone() {
130
- return new Response(this._body, { status: this.status, statusText: this.statusText, headers: this.headers });
131
+ return new Response(this._body, { status: this.status, statusText: this.statusText, headers: new Headers(this.headers) });
131
132
  }
132
133
  };
133
134
  }
package/skgo-adapter.js CHANGED
@@ -207,8 +207,10 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
207
207
  checkRemoteIds(`${out}/client`, generated.remotes, hashes);
208
208
 
209
209
  builder.writePrerendered(`${out}/prerendered`);
210
- for (const file of walk(`${out}/prerendered`)) {
211
- if (file.endsWith('.html')) authorizePrerenderedScripts(file);
210
+ if (existsSync(`${out}/prerendered`)) {
211
+ for (const file of walk(`${out}/prerendered`)) {
212
+ if (file.endsWith('.html')) authorizePrerenderedScripts(file);
213
+ }
212
214
  }
213
215
  const prerendered = readPrerendered(builder);
214
216
  // The SPA shell is still emitted: it is what a page whose branch turns
@@ -241,7 +243,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
241
243
  // `precompress: true` default.
242
244
  if (precompress) {
243
245
  await builder.compress(`${out}/client`);
244
- await builder.compress(`${out}/prerendered`);
246
+ if (existsSync(`${out}/prerendered`)) await builder.compress(`${out}/prerendered`);
245
247
  }
246
248
 
247
249
  // `builder` has no writeJson in kit 3.0.0-next.27 (it existed on the