@sveltejs/kit 1.0.0-next.537 → 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.537",
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,
@@ -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);
@@ -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;