@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/kit",
3
- "version": "2.34.0",
3
+ "version": "2.35.0",
4
4
  "description": "SvelteKit is the fastest way to build Svelte apps",
5
5
  "keywords": [
6
6
  "framework",
@@ -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 {Record<string, import('./page/types.js').Cookie>} */
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
- const c = new_cookies[name];
64
- if (
65
- c &&
66
- domain_matches(url.hostname, c.options.domain) &&
67
- path_matches(url.pathname, c.options.path)
68
- ) {
69
- return c.value;
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
- for (const c of Object.values(new_cookies)) {
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
- cookies[c.name] = c.value;
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 key in new_cookies) {
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
- new_cookies[name] = { name, value, options: { ...options, path } };
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, new_cookies[name].options);
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[]} cookies
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
- console.error((status === 404 && /** @type {Error} */ (error)?.message) || error)),
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
- return JSON.parse(await text());
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
- /** @type {Set<string>} */
76
- const link_header_preloads = new Set();
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
- const preload_atts = ['rel="preload"', 'as="style"'];
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
- head += `\n\t\t<link ${attributes.join(' ')}>`;
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
- link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
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 if (state.prerendering) {
329
- head += `\n\t\t<link rel="modulepreload" href="${path}">`;
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 (link_header_preloads.size) {
549
- headers.set('link', Array.from(link_header_preloads).join(', '));
561
+ if (link_headers.size) {
562
+ headers.set('link', Array.from(link_headers).join(', '));
550
563
  }
551
564
  }
552
565
 
@@ -6,7 +6,7 @@ export interface Fetched {
6
6
  method: string;
7
7
  request_body?: string | ArrayBufferView | null;
8
8
  request_headers?: HeadersInit | undefined;
9
- response_body: string;
9
+ response_body: string | undefined;
10
10
  response: Response;
11
11
  is_b64?: boolean;
12
12
  }
@@ -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, Object.values(new_cookies));
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, Object.values(new_cookies));
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
- return error.body;
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
@@ -1,4 +1,4 @@
1
1
  // generated during release, do not modify
2
2
 
3
3
  /** @type {string} */
4
- export const VERSION = '2.34.0';
4
+ export const VERSION = '2.35.0';