@sveltejs/kit 1.0.0-next.536 → 1.0.0-next.538

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": "1.0.0-next.536",
3
+ "version": "1.0.0-next.538",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/sveltejs/kit",
@@ -1,5 +1,11 @@
1
1
  import { onMount, tick } from 'svelte';
2
- import { make_trackable, decode_params, normalize_path, add_data_suffix } from '../../utils/url.js';
2
+ import {
3
+ make_trackable,
4
+ decode_pathname,
5
+ decode_params,
6
+ normalize_path,
7
+ add_data_suffix
8
+ } from '../../utils/url.js';
3
9
  import { find_anchor, get_base_uri, scroll_state } from './utils.js';
4
10
  import {
5
11
  lock_fetch,
@@ -83,10 +89,10 @@ export function create_client({ target, base, trailing_slash }) {
83
89
  let load_cache = null;
84
90
 
85
91
  const callbacks = {
86
- /** @type {Array<(navigation: import('types').Navigation & { cancel: () => void }) => void>} */
92
+ /** @type {Array<(navigation: import('types').BeforeNavigate) => void>} */
87
93
  before_navigate: [],
88
94
 
89
- /** @type {Array<(navigation: import('types').Navigation) => void>} */
95
+ /** @type {Array<(navigation: import('types').AfterNavigate) => void>} */
90
96
  after_navigate: []
91
97
  };
92
98
 
@@ -382,7 +388,7 @@ export function create_client({ target, base, trailing_slash }) {
382
388
  });
383
389
  post_update();
384
390
 
385
- /** @type {import('types').Navigation} */
391
+ /** @type {import('types').AfterNavigate} */
386
392
  const navigation = {
387
393
  from: null,
388
394
  to: add_url_properties('to', {
@@ -992,7 +998,7 @@ export function create_client({ target, base, trailing_slash }) {
992
998
  function get_navigation_intent(url, invalidating) {
993
999
  if (is_external_url(url)) return;
994
1000
 
995
- const path = decodeURI(url.pathname.slice(base.length) || '/');
1001
+ const path = decode_pathname(url.pathname.slice(base.length) || '/');
996
1002
 
997
1003
  for (const route of routes) {
998
1004
  const params = route.exec(path);
@@ -1116,7 +1122,9 @@ export function create_client({ target, base, trailing_slash }) {
1116
1122
  nav_token,
1117
1123
  () => {
1118
1124
  navigating = false;
1119
- callbacks.after_navigate.forEach((fn) => fn(navigation));
1125
+ callbacks.after_navigate.forEach((fn) =>
1126
+ fn(/** @type {import('types').AfterNavigate} */ (navigation))
1127
+ );
1120
1128
  stores.navigating.set(null);
1121
1129
  }
1122
1130
  );
@@ -1307,7 +1315,7 @@ export function create_client({ target, base, trailing_slash }) {
1307
1315
  if (!navigating) {
1308
1316
  // If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
1309
1317
  // it's due to an external or full-page-reload link, for which we don't want to call the hook again.
1310
- /** @type {import('types').Navigation & { cancel: () => void }} */
1318
+ /** @type {import('types').BeforeNavigate} */
1311
1319
  const navigation = {
1312
1320
  from: add_url_properties('from', {
1313
1321
  params: current.params,
@@ -15,9 +15,35 @@ const cookie_paths = {};
15
15
  */
16
16
  export function get_cookies(request, url, options) {
17
17
  const header = request.headers.get('cookie') ?? '';
18
-
19
18
  const initial_cookies = parse(header);
20
19
 
20
+ const normalized_url = normalize_path(
21
+ // Remove suffix: 'foo/__data.json' would mean the cookie path is '/foo',
22
+ // whereas a direct hit of /foo would mean the cookie path is '/'
23
+ has_data_suffix(url.pathname) ? strip_data_suffix(url.pathname) : url.pathname,
24
+ options.trailing_slash
25
+ );
26
+ // Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo'
27
+ const default_path = normalized_url.split('/').slice(0, -1).join('/') || '/';
28
+
29
+ if (options.dev) {
30
+ // Remove all cookies that no longer exist according to the request
31
+ for (const name of Object.keys(cookie_paths)) {
32
+ cookie_paths[name] = new Set(
33
+ [...cookie_paths[name]].filter(
34
+ (path) => !path_matches(normalized_url, path) || name in initial_cookies
35
+ )
36
+ );
37
+ }
38
+ // Add all new cookies we might not have seen before
39
+ for (const name in initial_cookies) {
40
+ cookie_paths[name] = cookie_paths[name] ?? new Set();
41
+ if (![...cookie_paths[name]].some((path) => path_matches(normalized_url, path))) {
42
+ cookie_paths[name].add(default_path);
43
+ }
44
+ }
45
+ }
46
+
21
47
  /** @type {Record<string, import('./page/types').Cookie>} */
22
48
  const new_cookies = {};
23
49
 
@@ -57,9 +83,14 @@ export function get_cookies(request, url, options) {
57
83
  return cookie;
58
84
  }
59
85
 
60
- if (c || cookie_paths[name]?.size > 0) {
86
+ const paths = new Set([...(cookie_paths[name] ?? [])]);
87
+ if (c) {
88
+ paths.add(c.options.path ?? default_path);
89
+ }
90
+ if (paths.size > 0) {
61
91
  console.warn(
62
- `Cookie with name '${name}' was not found, but a cookie with that name exists at a sub path. Did you mean to set its 'path' to '/'?`
92
+ // prettier-ignore
93
+ `Cookie with name '${name}' was not found at path '${url.pathname}', but a cookie with that name exists at these paths: '${[...paths].join("', '")}'. Did you mean to set its 'path' to '/' instead?`
63
94
  );
64
95
  }
65
96
  },
@@ -70,17 +101,7 @@ export function get_cookies(request, url, options) {
70
101
  * @param {import('cookie').CookieSerializeOptions} opts
71
102
  */
72
103
  set(name, value, opts = {}) {
73
- let path = opts.path;
74
- if (!path) {
75
- const normalized = normalize_path(
76
- // Remove suffix: 'foo/__data.json' would mean the cookie path is '/foo',
77
- // whereas a direct hit of /foo would mean the cookie path is '/'
78
- has_data_suffix(url.pathname) ? strip_data_suffix(url.pathname) : url.pathname,
79
- options.trailing_slash
80
- );
81
- // Emulate browser-behavior: if the cookie is set at '/foo/bar', its path is '/foo'
82
- path = normalized.split('/').slice(0, -1).join('/') || '/';
83
- }
104
+ let path = opts.path ?? default_path;
84
105
 
85
106
  new_cookies[name] = {
86
107
  name,
@@ -93,11 +114,12 @@ export function get_cookies(request, url, options) {
93
114
  };
94
115
 
95
116
  if (options.dev) {
96
- cookie_paths[name] = cookie_paths[name] || new Set();
117
+ cookie_paths[name] = cookie_paths[name] ?? new Set();
97
118
  if (!value) {
98
119
  if (!cookie_paths[name].has(path) && cookie_paths[name].size > 0) {
120
+ const paths = `'${Array.from(cookie_paths[name]).join("', '")}'`;
99
121
  console.warn(
100
- `Trying to delete cookie '${name}' at path '${path}', but a cookie with that name only exists at a different path.`
122
+ `Trying to delete cookie '${name}' at path '${path}', but a cookie with that name only exists at these paths: ${paths}.`
101
123
  );
102
124
  }
103
125
  cookie_paths[name].delete(path);
@@ -5,6 +5,8 @@ import { load_server_data } from '../page/load_data.js';
5
5
  import { clarify_devalue_error, handle_error_and_jsonify, serialize_data_node } from '../utils.js';
6
6
  import { normalize_path, strip_data_suffix } from '../../../utils/url.js';
7
7
 
8
+ export const INVALIDATED_HEADER = 'x-sveltekit-invalidated';
9
+
8
10
  /**
9
11
  * @param {import('types').RequestEvent} event
10
12
  * @param {import('types').SSRRoute} route
@@ -24,7 +26,7 @@ export async function render_data(event, route, options, state) {
24
26
  const node_ids = [...route.page.layouts, route.page.leaf];
25
27
 
26
28
  const invalidated =
27
- event.request.headers.get('x-sveltekit-invalidated')?.split(',').map(Boolean) ??
29
+ event.request.headers.get(INVALIDATED_HEADER)?.split(',').map(Boolean) ??
28
30
  node_ids.map(() => true);
29
31
 
30
32
  let aborted = false;
@@ -6,6 +6,7 @@ import { coalesce_to_error } from '../../utils/error.js';
6
6
  import { is_form_content_type } from '../../utils/http.js';
7
7
  import { GENERIC_ERROR, handle_fatal_error } from './utils.js';
8
8
  import {
9
+ decode_pathname,
9
10
  decode_params,
10
11
  disable_search,
11
12
  has_data_suffix,
@@ -13,7 +14,7 @@ import {
13
14
  strip_data_suffix
14
15
  } from '../../utils/url.js';
15
16
  import { exec } from '../../utils/routing.js';
16
- import { render_data } from './data/index.js';
17
+ import { INVALIDATED_HEADER, render_data } from './data/index.js';
17
18
  import { add_cookies_to_headers, get_cookies } from './cookie.js';
18
19
  import { HttpError } from '../control.js';
19
20
  import { create_fetch } from './fetch.js';
@@ -44,7 +45,7 @@ export async function respond(request, options, state) {
44
45
 
45
46
  let decoded;
46
47
  try {
47
- decoded = decodeURI(url.pathname);
48
+ decoded = decode_pathname(url.pathname);
48
49
  } catch {
49
50
  return new Response('Malformed URI', { status: 400 });
50
51
  }
@@ -296,14 +297,21 @@ export async function respond(request, options, state) {
296
297
  resolve(event, opts).then((response) => {
297
298
  // add headers/cookies here, rather than inside `resolve`, so that we
298
299
  // can do it once for all responses instead of once per `return`
299
- if (!is_data_request) {
300
- // we only want to set cookies on __data.json requests, we don't
301
- // want to cache stuff erroneously etc
302
- for (const key in headers) {
303
- const value = headers[key];
304
- response.headers.set(key, /** @type {string} */ (value));
300
+ for (const key in headers) {
301
+ const value = headers[key];
302
+ response.headers.set(key, /** @type {string} */ (value));
303
+ }
304
+
305
+ if (is_data_request) {
306
+ // set the Vary header on __data.json requests to ensure we don't cache
307
+ // incomplete responses with skipped data loads
308
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
309
+ const vary = response.headers.get('Vary');
310
+ if (vary !== '*') {
311
+ response.headers.append('Vary', INVALIDATED_HEADER);
305
312
  }
306
313
  }
314
+
307
315
  add_cookies_to_headers(response.headers, Object.values(new_cookies));
308
316
 
309
317
  if (state.prerendering && event.route.id !== null) {
@@ -1,7 +1,8 @@
1
1
  import { disable_search, make_trackable } from '../../../utils/url.js';
2
2
  import { unwrap_promises } from '../../../utils/promises.js';
3
+
3
4
  /**
4
- * Calls the user's `load` function.
5
+ * Calls the user's server `load` function.
5
6
  * @param {{
6
7
  * event: import('types').RequestEvent;
7
8
  * state: import('types').SSRState;
@@ -103,6 +104,7 @@ export async function load_data({
103
104
  data: server_data_node?.data ?? null,
104
105
  route: event.route,
105
106
  fetch: async (input, init) => {
107
+ const cloned_body = input instanceof Request && input.body ? input.clone().body : null;
106
108
  const response = await event.fetch(input, init);
107
109
 
108
110
  const url = new URL(input instanceof Request ? input.url : input, event.url);
@@ -149,7 +151,11 @@ export async function load_data({
149
151
  fetched.push({
150
152
  url: same_origin ? url.href.slice(event.url.origin.length) : url.href,
151
153
  method: event.request.method,
152
- request_body: /** @type {string | ArrayBufferView | undefined} */ (init?.body),
154
+ request_body: /** @type {string | ArrayBufferView | undefined} */ (
155
+ input instanceof Request && cloned_body
156
+ ? await stream_to_string(cloned_body)
157
+ : init?.body
158
+ ),
153
159
  response_body: body,
154
160
  response: response
155
161
  });
@@ -233,3 +239,20 @@ export async function load_data({
233
239
 
234
240
  return data ? unwrap_promises(data) : null;
235
241
  }
242
+
243
+ /**
244
+ * @param {ReadableStream<Uint8Array>} stream
245
+ */
246
+ async function stream_to_string(stream) {
247
+ let result = '';
248
+ const reader = stream.getReader();
249
+ const decoder = new TextDecoder();
250
+ while (true) {
251
+ const { done, value } = await reader.read();
252
+ if (done) {
253
+ break;
254
+ }
255
+ result += decoder.decode(value);
256
+ }
257
+ return result;
258
+ }
package/src/utils/url.js CHANGED
@@ -54,23 +54,20 @@ export function normalize_path(path, trailing_slash) {
54
54
  return path;
55
55
  }
56
56
 
57
+ /**
58
+ * Decode pathname excluding %25 to prevent further double decoding of params
59
+ * @param {string} pathname
60
+ */
61
+ export function decode_pathname(pathname) {
62
+ return pathname.split('%25').map(decodeURI).join('%25');
63
+ }
64
+
57
65
  /** @param {Record<string, string>} params */
58
66
  export function decode_params(params) {
59
67
  for (const key in params) {
60
68
  // input has already been decoded by decodeURI
61
- // now handle the rest that decodeURIComponent would do
62
- params[key] = params[key]
63
- .replace(/%23/g, '#')
64
- .replace(/%3[Bb]/g, ';')
65
- .replace(/%2[Cc]/g, ',')
66
- .replace(/%2[Ff]/g, '/')
67
- .replace(/%3[Ff]/g, '?')
68
- .replace(/%3[Aa]/g, ':')
69
- .replace(/%40/g, '@')
70
- .replace(/%26/g, '&')
71
- .replace(/%3[Dd]/g, '=')
72
- .replace(/%2[Bb]/g, '+')
73
- .replace(/%24/g, '$');
69
+ // now handle the rest
70
+ params[key] = decodeURIComponent(params[key]);
74
71
  }
75
72
 
76
73
  return params;
@@ -171,7 +171,7 @@ declare module '$app/forms' {
171
171
  * ```
172
172
  */
173
173
  declare module '$app/navigation' {
174
- import { Navigation } from '@sveltejs/kit';
174
+ import { BeforeNavigate, AfterNavigate } from '@sveltejs/kit';
175
175
 
176
176
  /**
177
177
  * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling.
@@ -261,16 +261,14 @@ declare module '$app/navigation' {
261
261
  *
262
262
  * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
263
263
  */
264
- export function beforeNavigate(
265
- callback: (navigation: Navigation & { cancel(): void }) => void
266
- ): void;
264
+ export function beforeNavigate(callback: (navigation: BeforeNavigate) => void): void;
267
265
 
268
266
  /**
269
267
  * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL.
270
268
  *
271
269
  * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
272
270
  */
273
- export function afterNavigate(callback: (navigation: Navigation) => void): void;
271
+ export function afterNavigate(callback: (navigation: AfterNavigate) => void): void;
274
272
  }
275
273
 
276
274
  /**
package/types/index.d.ts CHANGED
@@ -414,13 +414,12 @@ export interface Navigation {
414
414
  to: NavigationTarget | null;
415
415
  /**
416
416
  * The type of navigation:
417
- * - `enter`: The app has hydrated
418
417
  * - `leave`: The user is leaving the app by closing the tab or using the back/forward buttons to go to a different document
419
418
  * - `link`: Navigation was triggered by a link click
420
419
  * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect
421
420
  * - `popstate`: Navigation was triggered by back/forward navigation
422
421
  */
423
- type: NavigationType;
422
+ type: Omit<NavigationType, 'enter'>;
424
423
  /**
425
424
  * Whether or not the navigation will result in the page being unloaded (i.e. not a client-side navigation)
426
425
  */
@@ -431,6 +430,31 @@ export interface Navigation {
431
430
  delta?: number;
432
431
  }
433
432
 
433
+ /**
434
+ * The interface that corresponds to the `beforeNavigate`'s input parameter.
435
+ */
436
+ export interface BeforeNavigate extends Navigation {
437
+ /**
438
+ * Call this to prevent the navigation from starting.
439
+ */
440
+ cancel(): void;
441
+ }
442
+
443
+ /**
444
+ * The interface that corresponds to the `afterNavigate`'s input parameter.
445
+ */
446
+ export interface AfterNavigate extends Navigation {
447
+ /**
448
+ * The type of navigation:
449
+ * - `enter`: The app has hydrated
450
+ * - `link`: Navigation was triggered by a link click
451
+ * - `goto`: Navigation was triggered by a `goto(...)` call or a redirect
452
+ * - `popstate`: Navigation was triggered by back/forward navigation
453
+ */
454
+ type: Omit<NavigationType, 'leave'>;
455
+ willUnload: false;
456
+ }
457
+
434
458
  /**
435
459
  * The shape of the `$page` store
436
460
  */