@sveltejs/kit 2.50.2 → 2.52.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.
@@ -2,6 +2,7 @@
2
2
  /** @import { ResolveArgs } from './types.js' */
3
3
  import { base, assets, hash_routing } from './internal/client.js';
4
4
  import { resolve_route } from '../../../utils/routing.js';
5
+ import { get_navigation_intent } from '../../client/client.js';
5
6
 
6
7
  /**
7
8
  * Resolve the URL of an asset in your `static` directory, by prefixing it with [`config.kit.paths.assets`](https://svelte.dev/docs/kit/configuration#paths) if configured, or otherwise by prefixing it with the base path.
@@ -58,4 +59,41 @@ export function resolve(...args) {
58
59
  );
59
60
  }
60
61
 
62
+ /**
63
+ * Match a path or URL to a route ID and extracts any parameters.
64
+ *
65
+ * @example
66
+ * ```js
67
+ * import { match } from '$app/paths';
68
+ *
69
+ * const route = await match('/blog/hello-world');
70
+ *
71
+ * if (route?.id === '/blog/[slug]') {
72
+ * const slug = route.params.slug;
73
+ * const response = await fetch(`/api/posts/${slug}`);
74
+ * const post = await response.json();
75
+ * }
76
+ * ```
77
+ * @since 2.52.0
78
+ *
79
+ * @param {Pathname | URL | (string & {})} url
80
+ * @returns {Promise<{ id: RouteId, params: Record<string, string> } | null>}
81
+ */
82
+ export async function match(url) {
83
+ if (typeof url === 'string') {
84
+ url = new URL(url, location.href);
85
+ }
86
+
87
+ const intent = await get_navigation_intent(url, false);
88
+
89
+ if (intent) {
90
+ return {
91
+ id: /** @type {RouteId} */ (intent.route.id),
92
+ params: intent.params
93
+ };
94
+ }
95
+
96
+ return null;
97
+ }
98
+
61
99
  export { base, assets, resolve as resolveRoute };
@@ -1,7 +1,7 @@
1
1
  import { RouteId, Pathname, ResolvedPathname } from '$app/types';
2
2
  import { ResolveArgs } from './types.js';
3
3
 
4
- export { resolve, asset } from './client.js';
4
+ export { resolve, asset, match } from './client.js';
5
5
 
6
6
  /**
7
7
  * A string that matches [`config.kit.paths.base`](https://svelte.dev/docs/kit/configuration#paths).
@@ -1,6 +1,9 @@
1
1
  import { base, assets, relative, initial_base } from './internal/server.js';
2
- import { resolve_route } from '../../../utils/routing.js';
2
+ import { resolve_route, find_route } from '../../../utils/routing.js';
3
+ import { decode_pathname } from '../../../utils/url.js';
3
4
  import { try_get_request_store } from '@sveltejs/kit/internal/server';
5
+ import { manifest } from '__sveltekit/server';
6
+ import { get_hooks } from '__SERVER__/internal.js';
4
7
 
5
8
  /** @type {import('./client.js').asset} */
6
9
  export function asset(file) {
@@ -27,4 +30,42 @@ export function resolve(id, params) {
27
30
  return base + resolved;
28
31
  }
29
32
 
33
+ /** @type {import('./client.js').match} */
34
+ export async function match(url) {
35
+ const store = try_get_request_store();
36
+
37
+ if (typeof url === 'string') {
38
+ const origin = store?.event.url.origin ?? 'http://internal';
39
+ url = new URL(url, origin);
40
+ }
41
+
42
+ const { reroute } = await get_hooks();
43
+
44
+ let resolved_path = url.pathname;
45
+
46
+ try {
47
+ resolved_path = decode_pathname(
48
+ (await reroute?.({ url: new URL(url), fetch: store?.event.fetch ?? fetch })) ?? url.pathname
49
+ );
50
+ } catch {
51
+ return null;
52
+ }
53
+
54
+ if (base && resolved_path.startsWith(base)) {
55
+ resolved_path = resolved_path.slice(base.length) || '/';
56
+ }
57
+
58
+ const matchers = await manifest._.matchers();
59
+ const result = find_route(resolved_path, manifest._.routes, matchers);
60
+
61
+ if (result) {
62
+ return {
63
+ id: /** @type {import('$app/types').RouteId} */ (result.route.id),
64
+ params: result.params
65
+ };
66
+ }
67
+
68
+ return null;
69
+ }
70
+
30
71
  export { base, assets, resolve as resolveRoute };
@@ -605,7 +605,8 @@ async function initialize(result, target, hydrate) {
605
605
  to: {
606
606
  params: current.params,
607
607
  route: { id: current.route?.id ?? null },
608
- url: new URL(location.href)
608
+ url: new URL(location.href),
609
+ scroll: scroll_positions[current_history_index] ?? scroll_state()
609
610
  },
610
611
  willUnload: false,
611
612
  type: 'enter',
@@ -1400,7 +1401,7 @@ async function get_rerouted_url(url) {
1400
1401
  * @param {boolean} invalidating
1401
1402
  * @returns {Promise<import('./types.js').NavigationIntent | undefined>}
1402
1403
  */
1403
- async function get_navigation_intent(url, invalidating) {
1404
+ export async function get_navigation_intent(url, invalidating) {
1404
1405
  if (!url) return;
1405
1406
  if (is_external_url(url, base, app.hash)) return;
1406
1407
 
@@ -1463,12 +1464,13 @@ function get_page_key(url) {
1463
1464
  * intent?: import('./types.js').NavigationIntent;
1464
1465
  * delta?: number;
1465
1466
  * event?: PopStateEvent | MouseEvent;
1467
+ * scroll?: { x: number, y: number };
1466
1468
  * }} opts
1467
1469
  */
1468
- function _before_navigate({ url, type, intent, delta, event }) {
1470
+ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1469
1471
  let should_block = false;
1470
1472
 
1471
- const nav = create_navigation(current, intent, url, type);
1473
+ const nav = create_navigation(current, intent, url, type, scroll ?? null);
1472
1474
 
1473
1475
  if (delta !== undefined) {
1474
1476
  nav.navigation.delta = delta;
@@ -1543,6 +1545,7 @@ async function navigate({
1543
1545
  type,
1544
1546
  delta: popped?.delta,
1545
1547
  intent,
1548
+ scroll: popped?.scroll,
1546
1549
  // @ts-ignore
1547
1550
  event
1548
1551
  });
@@ -1758,26 +1761,18 @@ async function navigate({
1758
1761
  await svelte.tick();
1759
1762
 
1760
1763
  // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
1761
- let scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1764
+ /** @type {Element | null | ''} */
1765
+ let deep_linked = null;
1762
1766
 
1763
1767
  if (autoscroll) {
1764
- const deep_linked = url.hash && document.getElementById(get_id(url));
1768
+ const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1765
1769
  if (scroll) {
1766
1770
  scrollTo(scroll.x, scroll.y);
1767
- } else if (deep_linked) {
1771
+ } else if ((deep_linked = url.hash && document.getElementById(get_id(url)))) {
1768
1772
  // Here we use `scrollIntoView` on the element instead of `scrollTo`
1769
1773
  // because it natively supports the `scroll-margin` and `scroll-behavior`
1770
1774
  // CSS properties.
1771
1775
  deep_linked.scrollIntoView();
1772
-
1773
- // Get target position at this point because with smooth scrolling the scroll position
1774
- // retrieved from current x/y above might be wrong (since we might not have arrived at the destination yet)
1775
- const { top, left } = deep_linked.getBoundingClientRect();
1776
-
1777
- scroll = {
1778
- x: pageXOffset + left,
1779
- y: pageYOffset + top
1780
- };
1781
1776
  } else {
1782
1777
  scrollTo(0, 0);
1783
1778
  }
@@ -1791,7 +1786,10 @@ async function navigate({
1791
1786
  document.activeElement !== document.body;
1792
1787
 
1793
1788
  if (!keepfocus && !changed_focus) {
1794
- reset_focus(url, scroll);
1789
+ // We don't need to manually restore the scroll position if we're navigating
1790
+ // to a fragment identifier. It is automatically done for us when we set the
1791
+ // sequential navigation starting point with `location.replace`
1792
+ reset_focus(url, !deep_linked);
1795
1793
  }
1796
1794
 
1797
1795
  autoscroll = true;
@@ -1808,6 +1806,11 @@ async function navigate({
1808
1806
 
1809
1807
  nav.fulfil(undefined);
1810
1808
 
1809
+ // Update to.scroll to the actual scroll position after navigation completed
1810
+ if (nav.navigation.to) {
1811
+ nav.navigation.to.scroll = scroll_state();
1812
+ }
1813
+
1811
1814
  after_navigate_callbacks.forEach((fn) =>
1812
1815
  fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
1813
1816
  );
@@ -2991,9 +2994,9 @@ let resetting_focus = false;
2991
2994
 
2992
2995
  /**
2993
2996
  * @param {URL} url
2994
- * @param {{ x: number, y: number } | null} scroll
2997
+ * @param {boolean} [scroll]
2995
2998
  */
2996
- function reset_focus(url, scroll = null) {
2999
+ function reset_focus(url, scroll = true) {
2997
3000
  const autofocus = document.querySelector('[autofocus]');
2998
3001
  if (autofocus) {
2999
3002
  // @ts-ignore
@@ -3005,7 +3008,7 @@ function reset_focus(url, scroll = null) {
3005
3008
  // starting point to the fragment identifier.
3006
3009
  const id = get_id(url);
3007
3010
  if (id && document.getElementById(id)) {
3008
- const { x, y } = scroll ?? scroll_state();
3011
+ const { x, y } = scroll_state();
3009
3012
 
3010
3013
  // `element.focus()` doesn't work on Safari and Firefox Ubuntu so we need
3011
3014
  // to use this hack with `location.replace()` instead.
@@ -3013,24 +3016,16 @@ function reset_focus(url, scroll = null) {
3013
3016
  const history_state = history.state;
3014
3017
 
3015
3018
  resetting_focus = true;
3016
- location.replace(`#${id}`);
3019
+ location.replace(new URL(`#${id}`, location.href));
3017
3020
 
3018
- // if we're using hash routing, we need to restore the original hash after
3019
- // setting the focus with `location.replace()`. Although we're calling
3020
- // `location.replace()` again, the focus won't shift to the new hash
3021
- // unless there's an element with the ID `/pathname#hash`, etc.
3022
- if (app.hash) {
3023
- location.replace(url.hash);
3024
- }
3025
-
3026
- // but Firefox has a bug that sets the history state to `null` so we
3027
- // need to restore it after.
3028
- // See https://bugzilla.mozilla.org/show_bug.cgi?id=1199924
3029
- history.replaceState(history_state, '', url.hash);
3021
+ // Firefox has a bug that sets the history state to `null` so we need to
3022
+ // restore it after. See https://bugzilla.mozilla.org/show_bug.cgi?id=1199924
3023
+ // This is also needed to restore the original hash if we're using hash routing
3024
+ history.replaceState(history_state, '', url);
3030
3025
 
3031
- // Scroll management has already happened earlier so we need to restore
3026
+ // If scroll management has already happened earlier, we need to restore
3032
3027
  // the scroll position after setting the sequential focus navigation starting point
3033
- scrollTo(x, y);
3028
+ if (scroll) scrollTo(x, y);
3034
3029
  resetting_focus = false;
3035
3030
  });
3036
3031
  } else {
@@ -3102,8 +3097,9 @@ function reset_focus(url, scroll = null) {
3102
3097
  * @param {import('./types.js').NavigationIntent | undefined} intent
3103
3098
  * @param {URL | null} url
3104
3099
  * @param {T} type
3100
+ * @param {{ x: number, y: number } | null} [target_scroll] The scroll position for the target (for popstate navigations)
3105
3101
  */
3106
- function create_navigation(current, intent, url, type) {
3102
+ function create_navigation(current, intent, url, type, target_scroll = null) {
3107
3103
  /** @type {(value: any) => void} */
3108
3104
  let fulfil;
3109
3105
 
@@ -3123,12 +3119,14 @@ function create_navigation(current, intent, url, type) {
3123
3119
  from: {
3124
3120
  params: current.params,
3125
3121
  route: { id: current.route?.id ?? null },
3126
- url: current.url
3122
+ url: current.url,
3123
+ scroll: scroll_state()
3127
3124
  },
3128
3125
  to: url && {
3129
3126
  params: intent?.params ?? null,
3130
3127
  route: { id: intent?.route?.id ?? null },
3131
- url
3128
+ url,
3129
+ scroll: target_scroll
3132
3130
  },
3133
3131
  willUnload: !intent,
3134
3132
  type,
@@ -55,7 +55,12 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
55
55
  request.headers.delete('origin');
56
56
  }
57
57
 
58
- if (url.origin !== event.url.origin) {
58
+ const decoded = decodeURIComponent(url.pathname);
59
+
60
+ if (
61
+ url.origin !== event.url.origin ||
62
+ (paths.base && decoded !== paths.base && !decoded.startsWith(`${paths.base}/`))
63
+ ) {
59
64
  // Allow cookie passthrough for "credentials: same-origin" and "credentials: include"
60
65
  // if SvelteKit is serving my.domain.com:
61
66
  // - domain.com WILL NOT receive cookies
@@ -77,7 +82,6 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
77
82
  // handle fetch requests for static assets. e.g. prebaked data, etc.
78
83
  // we need to support everything the browser's fetch supports
79
84
  const prefix = paths.assets || paths.base;
80
- const decoded = decodeURIComponent(url.pathname);
81
85
  const filename = (
82
86
  decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded
83
87
  ).slice(1);
@@ -53,21 +53,30 @@ class BaseProvider {
53
53
  /** @type {import('types').CspDirectives} */
54
54
  #directives;
55
55
 
56
- /** @type {import('types').Csp.Source[]} */
56
+ /** @type {Set<import('types').Csp.Source>} */
57
57
  #script_src;
58
58
 
59
- /** @type {import('types').Csp.Source[]} */
59
+ /** @type {Set<import('types').Csp.Source>} */
60
60
  #script_src_elem;
61
61
 
62
- /** @type {import('types').Csp.Source[]} */
62
+ /** @type {Set<import('types').Csp.Source>} */
63
63
  #style_src;
64
64
 
65
- /** @type {import('types').Csp.Source[]} */
65
+ /** @type {Set<import('types').Csp.Source>} */
66
66
  #style_src_attr;
67
67
 
68
- /** @type {import('types').Csp.Source[]} */
68
+ /** @type {Set<import('types').Csp.Source>} */
69
69
  #style_src_elem;
70
70
 
71
+ /** @type {boolean} */
72
+ script_needs_nonce;
73
+
74
+ /** @type {boolean} */
75
+ style_needs_nonce;
76
+
77
+ /** @type {boolean} */
78
+ script_needs_hash;
79
+
71
80
  /** @type {string} */
72
81
  #nonce;
73
82
 
@@ -82,11 +91,11 @@ class BaseProvider {
82
91
 
83
92
  const d = this.#directives;
84
93
 
85
- this.#script_src = [];
86
- this.#script_src_elem = [];
87
- this.#style_src = [];
88
- this.#style_src_attr = [];
89
- this.#style_src_elem = [];
94
+ this.#script_src = new Set();
95
+ this.#script_src_elem = new Set();
96
+ this.#style_src = new Set();
97
+ this.#style_src_attr = new Set();
98
+ this.#style_src_elem = new Set();
90
99
 
91
100
  const effective_script_src = d['script-src'] || d['default-src'];
92
101
  const script_src_elem = d['script-src-elem'];
@@ -162,6 +171,7 @@ class BaseProvider {
162
171
 
163
172
  this.script_needs_nonce = this.#script_needs_csp && !this.#use_hashes;
164
173
  this.style_needs_nonce = this.#style_needs_csp && !this.#use_hashes;
174
+ this.script_needs_hash = this.#script_needs_csp && this.#use_hashes;
165
175
 
166
176
  this.#nonce = nonce;
167
177
  }
@@ -174,11 +184,23 @@ class BaseProvider {
174
184
  const source = this.#use_hashes ? `sha256-${sha256(content)}` : `nonce-${this.#nonce}`;
175
185
 
176
186
  if (this.#script_src_needs_csp) {
177
- this.#script_src.push(source);
187
+ this.#script_src.add(source);
178
188
  }
179
189
 
180
190
  if (this.#script_src_elem_needs_csp) {
181
- this.#script_src_elem.push(source);
191
+ this.#script_src_elem.add(source);
192
+ }
193
+ }
194
+
195
+ /** @param {`sha256-${string}`[]} hashes */
196
+ add_script_hashes(hashes) {
197
+ for (const hash of hashes) {
198
+ if (this.#script_src_needs_csp) {
199
+ this.#script_src.add(hash);
200
+ }
201
+ if (this.#script_src_elem_needs_csp) {
202
+ this.#script_src_elem.add(hash);
203
+ }
182
204
  }
183
205
  }
184
206
 
@@ -190,11 +212,11 @@ class BaseProvider {
190
212
  const source = this.#use_hashes ? `sha256-${sha256(content)}` : `nonce-${this.#nonce}`;
191
213
 
192
214
  if (this.#style_src_needs_csp) {
193
- this.#style_src.push(source);
215
+ this.#style_src.add(source);
194
216
  }
195
217
 
196
218
  if (this.#style_src_attr_needs_csp) {
197
- this.#style_src_attr.push(source);
219
+ this.#style_src_attr.add(source);
198
220
  }
199
221
 
200
222
  if (this.#style_src_elem_needs_csp) {
@@ -207,13 +229,13 @@ class BaseProvider {
207
229
  if (
208
230
  d['style-src-elem'] &&
209
231
  !d['style-src-elem'].includes(sha256_empty_comment_hash) &&
210
- !this.#style_src_elem.includes(sha256_empty_comment_hash)
232
+ !this.#style_src_elem.has(sha256_empty_comment_hash)
211
233
  ) {
212
- this.#style_src_elem.push(sha256_empty_comment_hash);
234
+ this.#style_src_elem.add(sha256_empty_comment_hash);
213
235
  }
214
236
 
215
237
  if (source !== sha256_empty_comment_hash) {
216
- this.#style_src_elem.push(source);
238
+ this.#style_src_elem.add(source);
217
239
  }
218
240
  }
219
241
  }
@@ -230,35 +252,35 @@ class BaseProvider {
230
252
 
231
253
  const directives = { ...this.#directives };
232
254
 
233
- if (this.#style_src.length > 0) {
255
+ if (this.#style_src.size > 0) {
234
256
  directives['style-src'] = [
235
257
  ...(directives['style-src'] || directives['default-src'] || []),
236
258
  ...this.#style_src
237
259
  ];
238
260
  }
239
261
 
240
- if (this.#style_src_attr.length > 0) {
262
+ if (this.#style_src_attr.size > 0) {
241
263
  directives['style-src-attr'] = [
242
264
  ...(directives['style-src-attr'] || []),
243
265
  ...this.#style_src_attr
244
266
  ];
245
267
  }
246
268
 
247
- if (this.#style_src_elem.length > 0) {
269
+ if (this.#style_src_elem.size > 0) {
248
270
  directives['style-src-elem'] = [
249
271
  ...(directives['style-src-elem'] || []),
250
272
  ...this.#style_src_elem
251
273
  ];
252
274
  }
253
275
 
254
- if (this.#script_src.length > 0) {
276
+ if (this.#script_src.size > 0) {
255
277
  directives['script-src'] = [
256
278
  ...(directives['script-src'] || directives['default-src'] || []),
257
279
  ...this.#script_src
258
280
  ];
259
281
  }
260
282
 
261
- if (this.#script_src_elem.length > 0) {
283
+ if (this.#script_src_elem.size > 0) {
262
284
  directives['script-src-elem'] = [
263
285
  ...(directives['script-src-elem'] || []),
264
286
  ...this.#script_src_elem
@@ -351,6 +373,10 @@ export class Csp {
351
373
  this.report_only_provider = new CspReportOnlyProvider(use_hashes, reportOnly, this.nonce);
352
374
  }
353
375
 
376
+ get script_needs_hash() {
377
+ return this.csp_provider.script_needs_hash || this.report_only_provider.script_needs_hash;
378
+ }
379
+
354
380
  get script_needs_nonce() {
355
381
  return this.csp_provider.script_needs_nonce || this.report_only_provider.script_needs_nonce;
356
382
  }
@@ -365,6 +391,12 @@ export class Csp {
365
391
  this.report_only_provider.add_script(content);
366
392
  }
367
393
 
394
+ /** @param {`sha256-${string}`[]} hashes */
395
+ add_script_hashes(hashes) {
396
+ this.csp_provider.add_script_hashes(hashes);
397
+ this.report_only_provider.add_script_hashes(hashes);
398
+ }
399
+
368
400
  /** @param {string} content */
369
401
  add_style(content) {
370
402
  this.csp_provider.add_style(content);
@@ -1,5 +1,5 @@
1
1
  import { text } from '@sveltejs/kit';
2
- import { Redirect } from '@sveltejs/kit/internal';
2
+ import { HttpError, Redirect } from '@sveltejs/kit/internal';
3
3
  import { compact } from '../../../utils/array.js';
4
4
  import { get_status, normalize_error } from '../../../utils/error.js';
5
5
  import { add_data_suffix } from '../../pathname.js';
@@ -357,6 +357,11 @@ export async function render_page(
357
357
  ssr === false ? server_data_serializer(event, event_state, options) : data_serializer
358
358
  });
359
359
  } catch (e) {
360
+ // a remote function could have thrown a redirect during render
361
+ if (e instanceof Redirect) {
362
+ return redirect_response(e.status, e.location);
363
+ }
364
+
360
365
  // if we end up here, it means the data loaded successfully
361
366
  // but the page failed to render, or that a prerendering error occurred
362
367
  return await respond_with_error({
@@ -365,7 +370,7 @@ export async function render_page(
365
370
  options,
366
371
  manifest,
367
372
  state,
368
- status: 500,
373
+ status: e instanceof HttpError ? e.status : 500,
369
374
  error: e,
370
375
  resolve_opts
371
376
  });