@sveltejs/kit 2.34.0 → 2.35.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 +1 -1
- package/src/core/postbuild/prerender.js +6 -6
- package/src/runtime/server/constants.js +1 -0
- package/src/runtime/server/cookie.js +52 -16
- package/src/runtime/server/index.js +9 -2
- package/src/runtime/server/page/load_data.js +9 -2
- package/src/runtime/server/page/render.js +30 -17
- package/src/runtime/server/page/types.d.ts +1 -1
- package/src/runtime/server/respond.js +2 -2
- package/src/runtime/server/utils.js +55 -1
- package/src/version.js +1 -1
package/package.json
CHANGED
|
@@ -125,6 +125,12 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
|
|
|
125
125
|
|
|
126
126
|
installPolyfills();
|
|
127
127
|
|
|
128
|
+
const server = new Server(manifest);
|
|
129
|
+
await server.init({
|
|
130
|
+
env,
|
|
131
|
+
read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`)
|
|
132
|
+
});
|
|
133
|
+
|
|
128
134
|
/** @type {Map<string, string>} */
|
|
129
135
|
const saved = new Map();
|
|
130
136
|
|
|
@@ -503,12 +509,6 @@ async function prerender({ hash, out, manifest_path, metadata, verbose, env }) {
|
|
|
503
509
|
|
|
504
510
|
log.info('Prerendering');
|
|
505
511
|
|
|
506
|
-
const server = new Server(manifest);
|
|
507
|
-
await server.init({
|
|
508
|
-
env,
|
|
509
|
-
read: (file) => createReadableStream(`${config.outDir}/output/server/${file}`)
|
|
510
|
-
});
|
|
511
|
-
|
|
512
512
|
for (const entry of config.prerender.entries) {
|
|
513
513
|
if (entry === '*') {
|
|
514
514
|
for (const [id, prerender] of prerender_map) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NULL_BODY_STATUS = [101, 103, 204, 205, 304];
|
|
@@ -27,6 +27,21 @@ function validate_options(options) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Generates a unique key for a cookie based on its domain, path, and name in
|
|
32
|
+
* the format: `<domain>/<path>?<name>`.
|
|
33
|
+
* If domain is undefined, it will be omitted.
|
|
34
|
+
* For example: `/?name`, `example.com/foo?name`.
|
|
35
|
+
*
|
|
36
|
+
* @param {string | undefined} domain
|
|
37
|
+
* @param {string} path
|
|
38
|
+
* @param {string} name
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function generate_cookie_key(domain, path, name) {
|
|
42
|
+
return `${domain || ''}${path}?${encodeURIComponent(name)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
/**
|
|
31
46
|
* @param {Request} request
|
|
32
47
|
* @param {URL} url
|
|
@@ -38,8 +53,8 @@ export function get_cookies(request, url) {
|
|
|
38
53
|
/** @type {string | undefined} */
|
|
39
54
|
let normalized_url;
|
|
40
55
|
|
|
41
|
-
/** @type {
|
|
42
|
-
const new_cookies =
|
|
56
|
+
/** @type {Map<string, import('./page/types.js').Cookie>} */
|
|
57
|
+
const new_cookies = new Map();
|
|
43
58
|
|
|
44
59
|
/** @type {import('cookie').CookieSerializeOptions} */
|
|
45
60
|
const defaults = {
|
|
@@ -60,13 +75,19 @@ export function get_cookies(request, url) {
|
|
|
60
75
|
* @param {import('cookie').CookieParseOptions} [opts]
|
|
61
76
|
*/
|
|
62
77
|
get(name, opts) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
c
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
// Look for the most specific matching cookie from new_cookies
|
|
79
|
+
const best_match = Array.from(new_cookies.values())
|
|
80
|
+
.filter((c) => {
|
|
81
|
+
return (
|
|
82
|
+
c.name === name &&
|
|
83
|
+
domain_matches(url.hostname, c.options.domain) &&
|
|
84
|
+
path_matches(url.pathname, c.options.path)
|
|
85
|
+
);
|
|
86
|
+
})
|
|
87
|
+
.sort((a, b) => b.options.path.length - a.options.path.length)[0];
|
|
88
|
+
|
|
89
|
+
if (best_match) {
|
|
90
|
+
return best_match.options.maxAge === 0 ? undefined : best_match.value;
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
const req_cookies = parse(header, { decode: opts?.decode });
|
|
@@ -98,15 +119,28 @@ export function get_cookies(request, url) {
|
|
|
98
119
|
getAll(opts) {
|
|
99
120
|
const cookies = parse(header, { decode: opts?.decode });
|
|
100
121
|
|
|
101
|
-
|
|
122
|
+
// Group cookies by name and find the most specific one for each name
|
|
123
|
+
const lookup = new Map();
|
|
124
|
+
|
|
125
|
+
for (const c of new_cookies.values()) {
|
|
102
126
|
if (
|
|
103
127
|
domain_matches(url.hostname, c.options.domain) &&
|
|
104
128
|
path_matches(url.pathname, c.options.path)
|
|
105
129
|
) {
|
|
106
|
-
|
|
130
|
+
const existing = lookup.get(c.name);
|
|
131
|
+
|
|
132
|
+
// If no existing cookie or this one has a more specific (longer) path, use this one
|
|
133
|
+
if (!existing || c.options.path.length > existing.options.path.length) {
|
|
134
|
+
lookup.set(c.name, c);
|
|
135
|
+
}
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
|
|
139
|
+
// Add the most specific cookies to the result
|
|
140
|
+
for (const c of lookup.values()) {
|
|
141
|
+
cookies[c.name] = c.value;
|
|
142
|
+
}
|
|
143
|
+
|
|
110
144
|
return Object.entries(cookies).map(([name, value]) => ({ name, value }));
|
|
111
145
|
},
|
|
112
146
|
|
|
@@ -172,8 +206,7 @@ export function get_cookies(request, url) {
|
|
|
172
206
|
};
|
|
173
207
|
|
|
174
208
|
// cookies previous set during this event with cookies.set have higher precedence
|
|
175
|
-
for (const
|
|
176
|
-
const cookie = new_cookies[key];
|
|
209
|
+
for (const cookie of new_cookies.values()) {
|
|
177
210
|
if (!domain_matches(destination.hostname, cookie.options.domain)) continue;
|
|
178
211
|
if (!path_matches(destination.pathname, cookie.options.path)) continue;
|
|
179
212
|
|
|
@@ -214,10 +247,13 @@ export function get_cookies(request, url) {
|
|
|
214
247
|
path = resolve(normalized_url, path);
|
|
215
248
|
}
|
|
216
249
|
|
|
217
|
-
|
|
250
|
+
// Generate unique key for cookie storage
|
|
251
|
+
const cookie_key = generate_cookie_key(options.domain, path, name);
|
|
252
|
+
const cookie = { name, value, options: { ...options, path } };
|
|
253
|
+
new_cookies.set(cookie_key, cookie);
|
|
218
254
|
|
|
219
255
|
if (__SVELTEKIT_DEV__) {
|
|
220
|
-
const serialized = serialize(name, value,
|
|
256
|
+
const serialized = serialize(name, value, cookie.options);
|
|
221
257
|
if (text_encoder.encode(serialized).byteLength > MAX_COOKIE_SIZE) {
|
|
222
258
|
throw new Error(`Cookie "${name}" is too large, and will be discarded by the browser`);
|
|
223
259
|
}
|
|
@@ -271,7 +307,7 @@ export function path_matches(path, constraint) {
|
|
|
271
307
|
|
|
272
308
|
/**
|
|
273
309
|
* @param {Headers} headers
|
|
274
|
-
* @param {import('./page/types.js').Cookie
|
|
310
|
+
* @param {MapIterator<import('./page/types.js').Cookie>} cookies
|
|
275
311
|
*/
|
|
276
312
|
export function add_cookies_to_headers(headers, cookies) {
|
|
277
313
|
for (const new_cookie of cookies) {
|
|
@@ -3,6 +3,7 @@ import { set_private_env, set_public_env } from '../shared-server.js';
|
|
|
3
3
|
import { options, get_hooks } from '__SERVER__/internal.js';
|
|
4
4
|
import { DEV } from 'esm-env';
|
|
5
5
|
import { filter_env } from '../../utils/env.js';
|
|
6
|
+
import { format_server_error } from './utils.js';
|
|
6
7
|
import { set_read_implementation, set_manifest } from '__sveltekit/server';
|
|
7
8
|
import { set_app } from './app.js';
|
|
8
9
|
|
|
@@ -87,8 +88,14 @@ export class Server {
|
|
|
87
88
|
handle: module.handle || (({ event, resolve }) => resolve(event)),
|
|
88
89
|
handleError:
|
|
89
90
|
module.handleError ||
|
|
90
|
-
(({ status, error }) =>
|
|
91
|
-
|
|
91
|
+
(({ status, error, event }) => {
|
|
92
|
+
const error_message = format_server_error(
|
|
93
|
+
status,
|
|
94
|
+
/** @type {Error} */ (error),
|
|
95
|
+
event
|
|
96
|
+
);
|
|
97
|
+
console.error(error_message);
|
|
98
|
+
}),
|
|
92
99
|
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)),
|
|
93
100
|
handleValidationError:
|
|
94
101
|
module.handleValidationError ||
|
|
@@ -6,6 +6,7 @@ import { with_request_store, merge_tracing } from '@sveltejs/kit/internal/server
|
|
|
6
6
|
import { record_span } from '../../telemetry/record_span.js';
|
|
7
7
|
import { clarify_devalue_error, get_node_type } from '../utils.js';
|
|
8
8
|
import { base64_encode, text_decoder } from '../../utils.js';
|
|
9
|
+
import { NULL_BODY_STATUS } from '../constants.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Calls the user's server `load` function.
|
|
@@ -345,7 +346,7 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
|
|
|
345
346
|
const proxy = new Proxy(response, {
|
|
346
347
|
get(response, key, _receiver) {
|
|
347
348
|
/**
|
|
348
|
-
* @param {string} body
|
|
349
|
+
* @param {string | undefined} body
|
|
349
350
|
* @param {boolean} is_b64
|
|
350
351
|
*/
|
|
351
352
|
async function push_fetched(body, is_b64) {
|
|
@@ -427,6 +428,11 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
|
|
|
427
428
|
async function text() {
|
|
428
429
|
const body = await response.text();
|
|
429
430
|
|
|
431
|
+
if (body === '' && NULL_BODY_STATUS.includes(response.status)) {
|
|
432
|
+
await push_fetched(undefined, false);
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
430
436
|
if (!body || typeof body === 'string') {
|
|
431
437
|
await push_fetched(body, false);
|
|
432
438
|
}
|
|
@@ -444,7 +450,8 @@ export function create_universal_fetch(event, state, fetched, csr, resolve_opts)
|
|
|
444
450
|
|
|
445
451
|
if (key === 'json') {
|
|
446
452
|
return async () => {
|
|
447
|
-
|
|
453
|
+
const body = await text();
|
|
454
|
+
return body ? JSON.parse(body) : undefined;
|
|
448
455
|
};
|
|
449
456
|
}
|
|
450
457
|
|
|
@@ -72,8 +72,18 @@ export async function render_response({
|
|
|
72
72
|
const stylesheets = new Set(client.stylesheets);
|
|
73
73
|
const fonts = new Set(client.fonts);
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The value of the Link header that is added to the response when not prerendering
|
|
77
|
+
* @type {Set<string>}
|
|
78
|
+
*/
|
|
79
|
+
const link_headers = new Set();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `<link>` tags that are added to prerendered responses
|
|
83
|
+
* (note that stylesheets are always added, prerendered or not)
|
|
84
|
+
* @type {Set<string>}
|
|
85
|
+
*/
|
|
86
|
+
const link_tags = new Set();
|
|
77
87
|
|
|
78
88
|
/** @type {Map<string, string>} */
|
|
79
89
|
// TODO if we add a client entry point one day, we will need to include inline_styles with the entry, otherwise stylesheets will be linked even if they are below inlineStyleThreshold
|
|
@@ -264,8 +274,7 @@ export async function render_response({
|
|
|
264
274
|
attributes.push('disabled', 'media="(max-width: 0)"');
|
|
265
275
|
} else {
|
|
266
276
|
if (resolve_opts.preload({ type: 'css', path })) {
|
|
267
|
-
|
|
268
|
-
link_header_preloads.add(`<${encodeURI(path)}>; ${preload_atts.join(';')}; nopush`);
|
|
277
|
+
link_headers.add(`<${encodeURI(path)}>; rel="preload"; as="style"; nopush`);
|
|
269
278
|
}
|
|
270
279
|
}
|
|
271
280
|
|
|
@@ -277,15 +286,12 @@ export async function render_response({
|
|
|
277
286
|
|
|
278
287
|
if (resolve_opts.preload({ type: 'font', path })) {
|
|
279
288
|
const ext = dep.slice(dep.lastIndexOf('.') + 1);
|
|
280
|
-
const attributes = [
|
|
281
|
-
'rel="preload"',
|
|
282
|
-
'as="font"',
|
|
283
|
-
`type="font/${ext}"`,
|
|
284
|
-
`href="${path}"`,
|
|
285
|
-
'crossorigin'
|
|
286
|
-
];
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
link_tags.add(`<link rel="preload" as="font" type="font/${ext}" href="${path}" crossorigin>`);
|
|
291
|
+
|
|
292
|
+
link_headers.add(
|
|
293
|
+
`<${encodeURI(path)}>; rel="preload"; as="font"; type="font/${ext}"; crossorigin; nopush`
|
|
294
|
+
);
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
297
|
|
|
@@ -322,15 +328,22 @@ export async function render_response({
|
|
|
322
328
|
|
|
323
329
|
for (const path of included_modulepreloads) {
|
|
324
330
|
// see the kit.output.preloadStrategy option for details on why we have multiple options here
|
|
325
|
-
|
|
331
|
+
link_headers.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
|
|
332
|
+
|
|
326
333
|
if (options.preload_strategy !== 'modulepreload') {
|
|
327
334
|
head += `\n\t\t<link rel="preload" as="script" crossorigin="anonymous" href="${path}">`;
|
|
328
|
-
} else
|
|
329
|
-
|
|
335
|
+
} else {
|
|
336
|
+
link_tags.add(`<link rel="modulepreload" href="${path}">`);
|
|
330
337
|
}
|
|
331
338
|
}
|
|
332
339
|
}
|
|
333
340
|
|
|
341
|
+
if (state.prerendering && link_tags.size > 0) {
|
|
342
|
+
head += Array.from(link_tags)
|
|
343
|
+
.map((tag) => `\n\t\t${tag}`)
|
|
344
|
+
.join('');
|
|
345
|
+
}
|
|
346
|
+
|
|
334
347
|
// prerender a `/path/to/page/__route.js` module
|
|
335
348
|
if (manifest._.client.routes && state.prerendering && !state.prerendering.fallback) {
|
|
336
349
|
const pathname = add_resolution_suffix(event.url.pathname);
|
|
@@ -545,8 +558,8 @@ export async function render_response({
|
|
|
545
558
|
headers.set('content-security-policy-report-only', report_only_header);
|
|
546
559
|
}
|
|
547
560
|
|
|
548
|
-
if (
|
|
549
|
-
headers.set('link', Array.from(
|
|
561
|
+
if (link_headers.size) {
|
|
562
|
+
headers.set('link', Array.from(link_headers).join(', '));
|
|
550
563
|
}
|
|
551
564
|
}
|
|
552
565
|
|
|
@@ -432,7 +432,7 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
432
432
|
response.headers.set(key, /** @type {string} */ (value));
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
add_cookies_to_headers(response.headers,
|
|
435
|
+
add_cookies_to_headers(response.headers, new_cookies.values());
|
|
436
436
|
|
|
437
437
|
if (state.prerendering && event.route.id !== null) {
|
|
438
438
|
response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id));
|
|
@@ -507,7 +507,7 @@ export async function internal_respond(request, options, manifest, state) {
|
|
|
507
507
|
: route?.page && is_action_json_request(event)
|
|
508
508
|
? action_json_redirect(e)
|
|
509
509
|
: redirect_response(e.status, e.location);
|
|
510
|
-
add_cookies_to_headers(response.headers,
|
|
510
|
+
add_cookies_to_headers(response.headers, new_cookies.values());
|
|
511
511
|
return response;
|
|
512
512
|
}
|
|
513
513
|
return await handle_fatal_error(event, event_state, options, e);
|
|
@@ -100,7 +100,8 @@ export async function handle_fatal_error(event, state, options, error) {
|
|
|
100
100
|
*/
|
|
101
101
|
export async function handle_error_and_jsonify(event, state, options, error) {
|
|
102
102
|
if (error instanceof HttpError) {
|
|
103
|
-
|
|
103
|
+
// @ts-expect-error custom user errors may not have a message field if App.Error is overwritten
|
|
104
|
+
return { message: 'Unknown Error', ...error.body };
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
if (__SVELTEKIT_DEV__ && typeof error == 'object') {
|
|
@@ -186,6 +187,59 @@ export function has_prerendered_path(manifest, pathname) {
|
|
|
186
187
|
);
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Formats the error into a nice message with sanitized stack trace
|
|
192
|
+
* @param {number} status
|
|
193
|
+
* @param {Error} error
|
|
194
|
+
* @param {import('@sveltejs/kit').RequestEvent} event
|
|
195
|
+
*/
|
|
196
|
+
export function format_server_error(status, error, event) {
|
|
197
|
+
const formatted_text = `\n\x1b[1;31m[${status}] ${event.request.method} ${event.url.pathname}\x1b[0m\n`;
|
|
198
|
+
|
|
199
|
+
if (status === 404) {
|
|
200
|
+
return formatted_text + error.message;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return formatted_text + (DEV ? clean_up_stack_trace(error) : error.stack);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* In dev, tidy up stack traces by making paths relative to the current project directory
|
|
208
|
+
* @param {string} file
|
|
209
|
+
*/
|
|
210
|
+
let relative = (file) => file;
|
|
211
|
+
|
|
212
|
+
if (DEV) {
|
|
213
|
+
try {
|
|
214
|
+
const path = await import('node:path');
|
|
215
|
+
const process = await import('node:process');
|
|
216
|
+
|
|
217
|
+
relative = (file) => path.relative(process.cwd(), file);
|
|
218
|
+
} catch {
|
|
219
|
+
// do nothing
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Provides a refined stack trace by excluding lines following the last occurrence of a line containing +page. +layout. or +server.
|
|
225
|
+
* @param {Error} error
|
|
226
|
+
*/
|
|
227
|
+
export function clean_up_stack_trace(error) {
|
|
228
|
+
const stack_trace = (error.stack?.split('\n') ?? []).map((line) => {
|
|
229
|
+
return line.replace(/\((.+)(:\d+:\d+)\)$/, (_, file, loc) => `(${relative(file)}${loc})`);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// progressive enhancement for people who haven't configured kit.files.src to something else
|
|
233
|
+
const last_line_from_src_code = stack_trace.findLastIndex((line) => /\(src[\\/]/.test(line));
|
|
234
|
+
|
|
235
|
+
if (last_line_from_src_code === -1) {
|
|
236
|
+
// default to the whole stack trace
|
|
237
|
+
return error.stack;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return stack_trace.slice(0, last_line_from_src_code + 1).join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
189
243
|
/**
|
|
190
244
|
* Returns the filename without the extension. e.g., `+page.server`, `+page`, etc.
|
|
191
245
|
* @param {string | undefined} node_id
|
package/src/version.js
CHANGED