@sveltejs/kit 1.30.2 → 2.0.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.
Files changed (57) hide show
  1. package/package.json +24 -24
  2. package/src/core/adapt/builder.js +8 -1
  3. package/src/core/config/index.js +9 -1
  4. package/src/core/config/options.js +1 -12
  5. package/src/core/postbuild/analyse.js +98 -80
  6. package/src/core/postbuild/prerender.js +11 -9
  7. package/src/core/sync/sync.js +2 -0
  8. package/src/core/sync/write_non_ambient.js +42 -0
  9. package/src/core/sync/write_server.js +3 -3
  10. package/src/core/sync/write_tsconfig.js +27 -78
  11. package/src/core/sync/write_types/index.js +1 -1
  12. package/src/exports/hooks/sequence.js +1 -1
  13. package/src/exports/index.js +88 -71
  14. package/src/exports/node/index.js +21 -24
  15. package/src/exports/node/polyfills.js +5 -34
  16. package/src/exports/public.d.ts +82 -61
  17. package/src/exports/vite/dev/index.js +11 -19
  18. package/src/exports/vite/graph_analysis/index.js +2 -4
  19. package/src/exports/vite/index.js +73 -14
  20. package/src/exports/vite/module_ids.js +7 -0
  21. package/src/exports/vite/preview/index.js +56 -130
  22. package/src/runtime/app/forms.js +2 -35
  23. package/src/runtime/app/navigation.js +33 -18
  24. package/src/runtime/app/paths.js +2 -29
  25. package/src/runtime/client/client.js +449 -199
  26. package/src/runtime/client/constants.js +5 -1
  27. package/src/runtime/client/session-storage.js +7 -5
  28. package/src/runtime/client/singletons.js +7 -1
  29. package/src/runtime/client/types.d.ts +6 -2
  30. package/src/runtime/client/utils.js +12 -10
  31. package/src/runtime/control.js +16 -8
  32. package/src/runtime/server/cookie.js +38 -61
  33. package/src/runtime/server/data/index.js +6 -4
  34. package/src/runtime/server/env_module.js +29 -0
  35. package/src/runtime/server/fetch.js +7 -6
  36. package/src/runtime/server/index.js +23 -20
  37. package/src/runtime/server/page/actions.js +24 -15
  38. package/src/runtime/server/page/index.js +6 -8
  39. package/src/runtime/server/page/load_data.js +58 -40
  40. package/src/runtime/server/page/render.js +12 -7
  41. package/src/runtime/server/page/respond_with_error.js +4 -4
  42. package/src/runtime/server/page/types.d.ts +1 -1
  43. package/src/runtime/server/respond.js +14 -12
  44. package/src/runtime/server/utils.js +11 -8
  45. package/src/runtime/shared-server.js +19 -2
  46. package/src/types/ambient.d.ts +7 -1
  47. package/src/types/internal.d.ts +4 -1
  48. package/src/types/synthetic/$env+dynamic+private.md +2 -0
  49. package/src/types/synthetic/$env+dynamic+public.md +2 -0
  50. package/src/utils/error.js +17 -1
  51. package/src/utils/routing.js +47 -1
  52. package/src/utils/url.js +45 -27
  53. package/src/version.js +1 -1
  54. package/types/index.d.ts +171 -118
  55. package/types/index.d.ts.map +9 -5
  56. package/src/utils/platform.js +0 -1
  57. package/src/utils/promises.js +0 -61
@@ -4,6 +4,7 @@ import {
4
4
  add_data_suffix,
5
5
  decode_params,
6
6
  decode_pathname,
7
+ strip_hash,
7
8
  make_trackable,
8
9
  normalize_path
9
10
  } from '../../utils/url.js';
@@ -18,43 +19,109 @@ import { parse } from './parse.js';
18
19
  import * as storage from './session-storage.js';
19
20
  import {
20
21
  find_anchor,
21
- get_base_uri,
22
+ resolve_url,
22
23
  get_link_info,
23
24
  get_router_options,
24
25
  is_external_url,
25
- scroll_state,
26
- origin
26
+ origin,
27
+ scroll_state
27
28
  } from './utils.js';
28
29
 
29
30
  import { base } from '__sveltekit/paths';
30
31
  import * as devalue from 'devalue';
31
- import { compact } from '../../utils/array.js';
32
+ import {
33
+ HISTORY_INDEX,
34
+ NAVIGATION_INDEX,
35
+ PRELOAD_PRIORITIES,
36
+ SCROLL_KEY,
37
+ STATES_KEY,
38
+ SNAPSHOT_KEY,
39
+ PAGE_URL_KEY
40
+ } from './constants.js';
32
41
  import { validate_page_exports } from '../../utils/exports.js';
33
- import { unwrap_promises } from '../../utils/promises.js';
34
- import { HttpError, Redirect, NotFound } from '../control.js';
42
+ import { compact } from '../../utils/array.js';
43
+ import { HttpError, Redirect, SvelteKitError } from '../control.js';
35
44
  import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js';
36
- import { INDEX_KEY, PRELOAD_PRIORITIES, SCROLL_KEY, SNAPSHOT_KEY } from './constants.js';
37
45
  import { stores } from './singletons.js';
46
+ import { get_message, get_status } from '../../utils/error.js';
38
47
 
39
48
  let errored = false;
40
49
 
50
+ /** @typedef {{ x: number; y: number }} ScrollPosition */
51
+
41
52
  // We track the scroll position associated with each history entry in sessionStorage,
42
53
  // rather than on history.state itself, because when navigation is driven by
43
54
  // popstate it's too late to update the scroll position associated with the
44
55
  // state we're navigating from
45
-
46
- /** @typedef {{ x: number, y: number }} ScrollPosition */
47
- /** @type {Record<number, ScrollPosition>} */
56
+ /**
57
+ * history index -> { x, y }
58
+ * @type {Record<number, ScrollPosition>}
59
+ */
48
60
  const scroll_positions = storage.get(SCROLL_KEY) ?? {};
49
61
 
50
- /** @type {Record<string, any[]>} */
62
+ /**
63
+ * history index -> any
64
+ * @type {Record<string, Record<string, any>>}
65
+ */
66
+ const states = storage.get(STATES_KEY, devalue.parse) ?? {};
67
+
68
+ /**
69
+ * navigation index -> any
70
+ * @type {Record<string, any[]>}
71
+ */
51
72
  const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
52
73
 
74
+ const original_push_state = history.pushState;
75
+ const original_replace_state = history.replaceState;
76
+
77
+ if (DEV) {
78
+ let warned = false;
79
+
80
+ const warn = () => {
81
+ if (warned) return;
82
+ warned = true;
83
+
84
+ console.warn(
85
+ "Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use the `pushState` and `replaceState` imports from `$app/navigation` instead."
86
+ );
87
+ };
88
+
89
+ history.pushState = (...args) => {
90
+ warn();
91
+ return original_push_state.apply(history, args);
92
+ };
93
+
94
+ history.replaceState = (...args) => {
95
+ warn();
96
+ return original_replace_state.apply(history, args);
97
+ };
98
+ }
99
+
53
100
  /** @param {number} index */
54
101
  function update_scroll_positions(index) {
55
102
  scroll_positions[index] = scroll_state();
56
103
  }
57
104
 
105
+ /**
106
+ * @param {number} current_history_index
107
+ * @param {number} current_navigation_index
108
+ */
109
+ function clear_onward_history(current_history_index, current_navigation_index) {
110
+ // if we navigated back, then pushed a new state, we can
111
+ // release memory by pruning the scroll/snapshot lookup
112
+ let i = current_history_index + 1;
113
+ while (scroll_positions[i]) {
114
+ delete scroll_positions[i];
115
+ i += 1;
116
+ }
117
+
118
+ i = current_navigation_index + 1;
119
+ while (snapshots[i]) {
120
+ delete snapshots[i];
121
+ i += 1;
122
+ }
123
+ }
124
+
58
125
  /**
59
126
  * Loads `href` the old-fashioned way, with a full page reload.
60
127
  * Returns a `Promise` that never resolves (to prevent any
@@ -66,6 +133,8 @@ function native_navigation(url) {
66
133
  return new Promise(() => {});
67
134
  }
68
135
 
136
+ function noop() {}
137
+
69
138
  /**
70
139
  * @param {import('./types.js').SvelteKitApp} app
71
140
  * @param {HTMLElement} target
@@ -83,6 +152,7 @@ export function create_client(app, target) {
83
152
  default_error_loader();
84
153
 
85
154
  const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
155
+
86
156
  /** @type {Array<((url: URL) => boolean)>} */
87
157
  const invalidated = [];
88
158
 
@@ -123,6 +193,8 @@ export function create_client(app, target) {
123
193
  let updating = false;
124
194
  let navigating = false;
125
195
  let hash_navigating = false;
196
+ /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
197
+ let has_navigated = false;
126
198
 
127
199
  let force_invalidation = false;
128
200
 
@@ -130,16 +202,25 @@ export function create_client(app, target) {
130
202
  let root;
131
203
 
132
204
  // keeping track of the history index in order to prevent popstate navigation events if needed
133
- let current_history_index = history.state?.[INDEX_KEY];
205
+ /** @type {number} */
206
+ let current_history_index = history.state?.[HISTORY_INDEX];
207
+
208
+ /** @type {number} */
209
+ let current_navigation_index = history.state?.[NAVIGATION_INDEX];
134
210
 
135
211
  if (!current_history_index) {
136
212
  // we use Date.now() as an offset so that cross-document navigations
137
213
  // within the app don't result in data loss
138
- current_history_index = Date.now();
214
+ current_history_index = current_navigation_index = Date.now();
139
215
 
140
216
  // create initial history entry, so we can return here
141
- history.replaceState(
142
- { ...history.state, [INDEX_KEY]: current_history_index },
217
+ original_replace_state.call(
218
+ history,
219
+ {
220
+ ...history.state,
221
+ [HISTORY_INDEX]: current_history_index,
222
+ [NAVIGATION_INDEX]: current_navigation_index
223
+ },
143
224
  '',
144
225
  location.href
145
226
  );
@@ -166,13 +247,12 @@ export function create_client(app, target) {
166
247
  // Accept all invalidations as they come, don't swallow any while another invalidation
167
248
  // is running because subsequent invalidations may make earlier ones outdated,
168
249
  // but batch multiple synchronous invalidations.
169
- pending_invalidate = pending_invalidate || Promise.resolve();
170
- await pending_invalidate;
250
+ await (pending_invalidate ||= Promise.resolve());
171
251
  if (!pending_invalidate) return;
172
252
  pending_invalidate = null;
173
253
 
174
- const url = new URL(location.href);
175
- const intent = get_navigation_intent(url, true);
254
+ const intent = get_navigation_intent(current.url, true);
255
+
176
256
  // Clear preload, it might be affected by the invalidation.
177
257
  // Also solves an edge case where a preload is triggered, the navigation for it
178
258
  // was then triggered and is still running while the invalidation kicks in,
@@ -185,7 +265,7 @@ export function create_client(app, target) {
185
265
 
186
266
  if (navigation_result) {
187
267
  if (navigation_result.type === 'redirect') {
188
- return goto(new URL(navigation_result.location, url).href, {}, 1, nav_token);
268
+ await goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token);
189
269
  } else {
190
270
  if (navigation_result.props.page !== undefined) {
191
271
  page = navigation_result.props.page;
@@ -193,6 +273,8 @@ export function create_client(app, target) {
193
273
  root.$set(navigation_result.props);
194
274
  }
195
275
  }
276
+
277
+ invalidated.length = 0;
196
278
  }
197
279
 
198
280
  /** @param {number} index */
@@ -213,49 +295,31 @@ export function create_client(app, target) {
213
295
  update_scroll_positions(current_history_index);
214
296
  storage.set(SCROLL_KEY, scroll_positions);
215
297
 
216
- capture_snapshot(current_history_index);
298
+ capture_snapshot(current_navigation_index);
217
299
  storage.set(SNAPSHOT_KEY, snapshots);
300
+ storage.set(STATES_KEY, states, devalue.stringify);
218
301
  }
219
302
 
220
303
  /**
221
304
  * @param {string | URL} url
222
- * @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean }} opts
305
+ * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; }} options
223
306
  * @param {number} redirect_count
224
307
  * @param {{}} [nav_token]
225
308
  */
226
- async function goto(
227
- url,
228
- {
229
- noScroll = false,
230
- replaceState = false,
231
- keepFocus = false,
232
- state = {},
233
- invalidateAll = false
234
- },
235
- redirect_count,
236
- nav_token
237
- ) {
238
- if (typeof url === 'string') {
239
- url = new URL(url, get_base_uri(document));
240
- }
241
-
309
+ async function goto(url, options, redirect_count, nav_token) {
242
310
  return navigate({
243
- url,
244
- scroll: noScroll ? scroll_state() : null,
245
- keepfocus: keepFocus,
311
+ type: 'goto',
312
+ url: resolve_url(url),
313
+ keepfocus: options.keepFocus,
314
+ noscroll: options.noScroll,
315
+ replace_state: options.replaceState,
246
316
  redirect_count,
247
- details: {
248
- state,
249
- replaceState
250
- },
251
317
  nav_token,
252
- accepted: () => {
253
- if (invalidateAll) {
318
+ accept: () => {
319
+ if (options.invalidateAll) {
254
320
  force_invalidation = true;
255
321
  }
256
- },
257
- blocked: () => {},
258
- type: 'goto'
322
+ }
259
323
  });
260
324
  }
261
325
 
@@ -275,19 +339,13 @@ export function create_client(app, target) {
275
339
  return load_cache.promise;
276
340
  }
277
341
 
278
- /** @param {...string} pathnames */
279
- async function preload_code(...pathnames) {
280
- if (DEV && pathnames.length > 1) {
281
- console.warn('Calling `preloadCode` with multiple arguments is deprecated');
282
- }
283
-
284
- const matching = routes.filter((route) => pathnames.some((pathname) => route.exec(pathname)));
285
-
286
- const promises = matching.map((r) => {
287
- return Promise.all([...r.layouts, r.leaf].map((load) => load?.[1]()));
288
- });
342
+ /** @param {string} pathname */
343
+ async function preload_code(pathname) {
344
+ const route = routes.find((route) => route.exec(get_url_path(pathname)));
289
345
 
290
- await Promise.all(promises);
346
+ if (route) {
347
+ await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]()));
348
+ }
291
349
  }
292
350
 
293
351
  /** @param {import('./types.js').NavigationFinished} result */
@@ -307,7 +365,7 @@ export function create_client(app, target) {
307
365
  hydrate: true
308
366
  });
309
367
 
310
- restore_snapshot(current_history_index);
368
+ restore_snapshot(current_navigation_index);
311
369
 
312
370
  /** @type {import('@sveltejs/kit').AfterNavigate} */
313
371
  const navigation = {
@@ -368,7 +426,8 @@ export function create_client(app, target) {
368
426
  },
369
427
  props: {
370
428
  // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
371
- constructors: compact(branch).map((branch_node) => branch_node.node.component)
429
+ constructors: compact(branch).map((branch_node) => branch_node.node.component),
430
+ page
372
431
  }
373
432
  };
374
433
 
@@ -412,6 +471,7 @@ export function create_client(app, target) {
412
471
  route: {
413
472
  id: route?.id ?? null
414
473
  },
474
+ state: {},
415
475
  status,
416
476
  url: new URL(url),
417
477
  form: form ?? null,
@@ -441,13 +501,16 @@ export function create_client(app, target) {
441
501
  /** @type {Record<string, any> | null} */
442
502
  let data = null;
443
503
 
504
+ let is_tracking = true;
505
+
444
506
  /** @type {import('types').Uses} */
445
507
  const uses = {
446
508
  dependencies: new Set(),
447
509
  params: new Set(),
448
510
  parent: false,
449
511
  route: false,
450
- url: false
512
+ url: false,
513
+ search_params: new Set()
451
514
  };
452
515
 
453
516
  const node = await loader();
@@ -471,20 +534,34 @@ export function create_client(app, target) {
471
534
  const load_input = {
472
535
  route: new Proxy(route, {
473
536
  get: (target, key) => {
474
- uses.route = true;
537
+ if (is_tracking) {
538
+ uses.route = true;
539
+ }
475
540
  return target[/** @type {'id'} */ (key)];
476
541
  }
477
542
  }),
478
543
  params: new Proxy(params, {
479
544
  get: (target, key) => {
480
- uses.params.add(/** @type {string} */ (key));
545
+ if (is_tracking) {
546
+ uses.params.add(/** @type {string} */ (key));
547
+ }
481
548
  return target[/** @type {string} */ (key)];
482
549
  }
483
550
  }),
484
551
  data: server_data_node?.data ?? null,
485
- url: make_trackable(url, () => {
486
- uses.url = true;
487
- }),
552
+ url: make_trackable(
553
+ url,
554
+ () => {
555
+ if (is_tracking) {
556
+ uses.url = true;
557
+ }
558
+ },
559
+ (param) => {
560
+ if (is_tracking) {
561
+ uses.search_params.add(param);
562
+ }
563
+ }
564
+ ),
488
565
  async fetch(resource, init) {
489
566
  /** @type {URL | string} */
490
567
  let requested;
@@ -520,7 +597,9 @@ export function create_client(app, target) {
520
597
 
521
598
  // we must fixup relative urls so they are resolved from the target page
522
599
  const resolved = new URL(requested, url);
523
- depends(resolved.href);
600
+ if (is_tracking) {
601
+ depends(resolved.href);
602
+ }
524
603
 
525
604
  // match ssr serialized data url, which is important to find cached responses
526
605
  if (resolved.origin === url.origin) {
@@ -535,8 +614,18 @@ export function create_client(app, target) {
535
614
  setHeaders: () => {}, // noop
536
615
  depends,
537
616
  parent() {
538
- uses.parent = true;
617
+ if (is_tracking) {
618
+ uses.parent = true;
619
+ }
539
620
  return parent();
621
+ },
622
+ untrack(fn) {
623
+ is_tracking = false;
624
+ try {
625
+ return fn();
626
+ } finally {
627
+ is_tracking = true;
628
+ }
540
629
  }
541
630
  };
542
631
 
@@ -550,10 +639,10 @@ export function create_client(app, target) {
550
639
  typeof data !== 'object'
551
640
  ? `a ${typeof data}`
552
641
  : data instanceof Response
553
- ? 'a Response object'
554
- : Array.isArray(data)
555
- ? 'an array'
556
- : 'a non-plain object'
642
+ ? 'a Response object'
643
+ : Array.isArray(data)
644
+ ? 'an array'
645
+ : 'a non-plain object'
557
646
  }, but must return a plain object at the top level (i.e. \`return {...}\`)`
558
647
  );
559
648
  }
@@ -563,7 +652,6 @@ export function create_client(app, target) {
563
652
  } else {
564
653
  data = (await node.universal.load.call(null, load_input)) ?? null;
565
654
  }
566
- data = data ? await unwrap_promises(data, route.id) : null;
567
655
  }
568
656
 
569
657
  return {
@@ -585,10 +673,18 @@ export function create_client(app, target) {
585
673
  * @param {boolean} parent_changed
586
674
  * @param {boolean} route_changed
587
675
  * @param {boolean} url_changed
676
+ * @param {Set<string>} search_params_changed
588
677
  * @param {import('types').Uses | undefined} uses
589
678
  * @param {Record<string, string>} params
590
679
  */
591
- function has_changed(parent_changed, route_changed, url_changed, uses, params) {
680
+ function has_changed(
681
+ parent_changed,
682
+ route_changed,
683
+ url_changed,
684
+ search_params_changed,
685
+ uses,
686
+ params
687
+ ) {
592
688
  if (force_invalidation) return true;
593
689
 
594
690
  if (!uses) return false;
@@ -597,6 +693,10 @@ export function create_client(app, target) {
597
693
  if (uses.route && route_changed) return true;
598
694
  if (uses.url && url_changed) return true;
599
695
 
696
+ for (const tracked_params of uses.search_params) {
697
+ if (search_params_changed.has(tracked_params)) return true;
698
+ }
699
+
600
700
  for (const param of uses.params) {
601
701
  if (params[param] !== current.params[param]) return true;
602
702
  }
@@ -619,6 +719,31 @@ export function create_client(app, target) {
619
719
  return null;
620
720
  }
621
721
 
722
+ /**
723
+ *
724
+ * @param {URL | null} old_url
725
+ * @param {URL} new_url
726
+ */
727
+ function diff_search_params(old_url, new_url) {
728
+ if (!old_url) return new Set(new_url.searchParams.keys());
729
+
730
+ const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]);
731
+
732
+ for (const key of changed) {
733
+ const old_values = old_url.searchParams.getAll(key);
734
+ const new_values = new_url.searchParams.getAll(key);
735
+
736
+ if (
737
+ old_values.every((value) => new_values.includes(value)) &&
738
+ new_values.every((value) => old_values.includes(value))
739
+ ) {
740
+ changed.delete(key);
741
+ }
742
+ }
743
+
744
+ return changed;
745
+ }
746
+
622
747
  /**
623
748
  * @param {import('./types.js').NavigationIntent} intent
624
749
  * @returns {Promise<import('./types.js').NavigationResult>}
@@ -640,9 +765,9 @@ export function create_client(app, target) {
640
765
 
641
766
  /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
642
767
  let server_data = null;
643
-
644
768
  const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
645
769
  const route_changed = current.route ? route.id !== current.route.id : false;
770
+ const search_params_changed = diff_search_params(current.url, url);
646
771
 
647
772
  let parent_invalid = false;
648
773
  const invalid_server_nodes = loaders.map((loader, i) => {
@@ -651,7 +776,14 @@ export function create_client(app, target) {
651
776
  const invalid =
652
777
  !!loader?.[0] &&
653
778
  (previous?.loader !== loader[1] ||
654
- has_changed(parent_invalid, route_changed, url_changed, previous.server?.uses, params));
779
+ has_changed(
780
+ parent_invalid,
781
+ route_changed,
782
+ url_changed,
783
+ search_params_changed,
784
+ previous.server?.uses,
785
+ params
786
+ ));
655
787
 
656
788
  if (invalid) {
657
789
  // For the next one
@@ -666,7 +798,7 @@ export function create_client(app, target) {
666
798
  server_data = await load_data(url, invalid_server_nodes);
667
799
  } catch (error) {
668
800
  return load_root_error_page({
669
- status: error instanceof HttpError ? error.status : 500,
801
+ status: get_status(error),
670
802
  error: await handle_error(error, { url, params, route: { id: route.id } }),
671
803
  url,
672
804
  route
@@ -694,7 +826,14 @@ export function create_client(app, target) {
694
826
  const valid =
695
827
  (!server_data_node || server_data_node.type === 'skip') &&
696
828
  loader[1] === previous?.loader &&
697
- !has_changed(parent_changed, route_changed, url_changed, previous.universal?.uses, params);
829
+ !has_changed(
830
+ parent_changed,
831
+ route_changed,
832
+ url_changed,
833
+ search_params_changed,
834
+ previous.universal?.uses,
835
+ params
836
+ );
698
837
  if (valid) return previous;
699
838
 
700
839
  parent_changed = true;
@@ -743,7 +882,7 @@ export function create_client(app, target) {
743
882
  };
744
883
  }
745
884
 
746
- let status = 500;
885
+ let status = get_status(err);
747
886
  /** @type {App.Error} */
748
887
  let error;
749
888
 
@@ -753,7 +892,6 @@ export function create_client(app, target) {
753
892
  status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
754
893
  error = /** @type {import('types').ServerErrorNode} */ (err).error;
755
894
  } else if (err instanceof HttpError) {
756
- status = err.status;
757
895
  error = err.body;
758
896
  } else {
759
897
  // Referenced node could have been removed due to redeploy, check
@@ -905,7 +1043,7 @@ export function create_client(app, target) {
905
1043
  function get_navigation_intent(url, invalidating) {
906
1044
  if (is_external_url(url, base)) return;
907
1045
 
908
- const path = get_url_path(url);
1046
+ const path = get_url_path(url.pathname);
909
1047
 
910
1048
  for (const route of routes) {
911
1049
  const params = route.exec(path);
@@ -919,9 +1057,9 @@ export function create_client(app, target) {
919
1057
  }
920
1058
  }
921
1059
 
922
- /** @param {URL} url */
923
- function get_url_path(url) {
924
- return decode_pathname(url.pathname.slice(base.length) || '/');
1060
+ /** @param {string} pathname */
1061
+ function get_url_path(pathname) {
1062
+ return decode_pathname(pathname.slice(base.length) || '/');
925
1063
  }
926
1064
 
927
1065
  /**
@@ -959,45 +1097,47 @@ export function create_client(app, target) {
959
1097
 
960
1098
  /**
961
1099
  * @param {{
962
- * url: URL;
963
- * scroll: { x: number, y: number } | null;
964
- * keepfocus: boolean;
965
- * redirect_count: number;
966
- * details: {
967
- * replaceState: boolean;
968
- * state: any;
969
- * } | null;
970
1100
  * type: import('@sveltejs/kit').Navigation["type"];
971
- * delta?: number;
1101
+ * url: URL;
1102
+ * popped?: {
1103
+ * state: Record<string, any>;
1104
+ * scroll: { x: number, y: number };
1105
+ * delta: number;
1106
+ * };
1107
+ * keepfocus?: boolean;
1108
+ * noscroll?: boolean;
1109
+ * replace_state?: boolean;
1110
+ * redirect_count?: number;
972
1111
  * nav_token?: {};
973
- * accepted: () => void;
974
- * blocked: () => void;
1112
+ * accept?: () => void;
1113
+ * block?: () => void;
975
1114
  * }} opts
976
1115
  */
977
1116
  async function navigate({
1117
+ type,
978
1118
  url,
979
- scroll,
1119
+ popped,
980
1120
  keepfocus,
981
- redirect_count,
982
- details,
983
- type,
984
- delta,
1121
+ noscroll,
1122
+ replace_state,
1123
+ redirect_count = 0,
985
1124
  nav_token = {},
986
- accepted,
987
- blocked
1125
+ accept = noop,
1126
+ block = noop
988
1127
  }) {
989
1128
  const intent = get_navigation_intent(url, false);
990
- const nav = before_navigate({ url, type, delta, intent });
1129
+ const nav = before_navigate({ url, type, delta: popped?.delta, intent });
991
1130
 
992
1131
  if (!nav) {
993
- blocked();
1132
+ block();
994
1133
  return;
995
1134
  }
996
1135
 
997
- // store this before calling `accepted()`, which may change the index
1136
+ // store this before calling `accept()`, which may change the index
998
1137
  const previous_history_index = current_history_index;
1138
+ const previous_navigation_index = current_navigation_index;
999
1139
 
1000
- accepted();
1140
+ accept();
1001
1141
 
1002
1142
  navigating = true;
1003
1143
 
@@ -1015,7 +1155,7 @@ export function create_client(app, target) {
1015
1155
  navigation_result = await server_fallback(
1016
1156
  url,
1017
1157
  { id: null },
1018
- await handle_error(new Error(`Not found: ${url.pathname}`), {
1158
+ await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), {
1019
1159
  url,
1020
1160
  params: {},
1021
1161
  route: { id: null }
@@ -1051,7 +1191,7 @@ export function create_client(app, target) {
1051
1191
  goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token);
1052
1192
  return false;
1053
1193
  }
1054
- } else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) {
1194
+ } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1055
1195
  const updated = await stores.updated.check();
1056
1196
  if (updated) {
1057
1197
  await native_navigation(url);
@@ -1066,36 +1206,39 @@ export function create_client(app, target) {
1066
1206
  updating = true;
1067
1207
 
1068
1208
  update_scroll_positions(previous_history_index);
1069
- capture_snapshot(previous_history_index);
1209
+ capture_snapshot(previous_navigation_index);
1070
1210
 
1071
1211
  // ensure the url pathname matches the page's trailing slash option
1072
- if (
1073
- navigation_result.props.page?.url &&
1074
- navigation_result.props.page.url.pathname !== url.pathname
1075
- ) {
1076
- url.pathname = navigation_result.props.page?.url.pathname;
1212
+ if (navigation_result.props.page.url.pathname !== url.pathname) {
1213
+ url.pathname = navigation_result.props.page.url.pathname;
1077
1214
  }
1078
1215
 
1079
- if (details) {
1080
- const change = details.replaceState ? 0 : 1;
1081
- details.state[INDEX_KEY] = current_history_index += change;
1082
- history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
1083
-
1084
- if (!details.replaceState) {
1085
- // if we navigated back, then pushed a new state, we can
1086
- // release memory by pruning the scroll/snapshot lookup
1087
- let i = current_history_index + 1;
1088
- while (snapshots[i] || scroll_positions[i]) {
1089
- delete snapshots[i];
1090
- delete scroll_positions[i];
1091
- i += 1;
1092
- }
1216
+ const state = popped ? popped.state : {};
1217
+
1218
+ if (!popped) {
1219
+ // this is a new navigation, rather than a popstate
1220
+ const change = replace_state ? 0 : 1;
1221
+
1222
+ const entry = {
1223
+ [HISTORY_INDEX]: (current_history_index += change),
1224
+ [NAVIGATION_INDEX]: (current_navigation_index += change)
1225
+ };
1226
+
1227
+ const fn = replace_state ? original_replace_state : original_push_state;
1228
+ fn.call(history, entry, '', url);
1229
+
1230
+ if (!replace_state) {
1231
+ clear_onward_history(current_history_index, current_navigation_index);
1093
1232
  }
1094
1233
  }
1095
1234
 
1235
+ states[current_history_index] = state;
1236
+
1096
1237
  // reset preload synchronously after the history state has been set to avoid race conditions
1097
1238
  load_cache = null;
1098
1239
 
1240
+ navigation_result.props.page.state = state;
1241
+
1099
1242
  if (started) {
1100
1243
  current = navigation_result.state;
1101
1244
 
@@ -1127,6 +1270,7 @@ export function create_client(app, target) {
1127
1270
  }
1128
1271
 
1129
1272
  root.$set(navigation_result.props);
1273
+ has_navigated = true;
1130
1274
  } else {
1131
1275
  initialize(navigation_result);
1132
1276
  }
@@ -1137,6 +1281,8 @@ export function create_client(app, target) {
1137
1281
  await tick();
1138
1282
 
1139
1283
  // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
1284
+ const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1285
+
1140
1286
  if (autoscroll) {
1141
1287
  const deep_linked =
1142
1288
  url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
@@ -1172,7 +1318,7 @@ export function create_client(app, target) {
1172
1318
  navigating = false;
1173
1319
 
1174
1320
  if (type === 'popstate') {
1175
- restore_snapshot(current_history_index);
1321
+ restore_snapshot(current_navigation_index);
1176
1322
  }
1177
1323
 
1178
1324
  nav.fulfil(undefined);
@@ -1247,9 +1393,7 @@ export function create_client(app, target) {
1247
1393
  (entries) => {
1248
1394
  for (const entry of entries) {
1249
1395
  if (entry.isIntersecting) {
1250
- preload_code(
1251
- get_url_path(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href))
1252
- );
1396
+ preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href);
1253
1397
  observer.unobserve(entry.target);
1254
1398
  }
1255
1399
  }
@@ -1290,7 +1434,7 @@ export function create_client(app, target) {
1290
1434
  }
1291
1435
  }
1292
1436
  } else if (priority <= options.preload_code) {
1293
- preload_code(get_url_path(/** @type {URL} */ (url)));
1437
+ preload_code(/** @type {URL} */ (url).pathname);
1294
1438
  }
1295
1439
  }
1296
1440
  }
@@ -1310,7 +1454,7 @@ export function create_client(app, target) {
1310
1454
  }
1311
1455
 
1312
1456
  if (options.preload_code === PRELOAD_PRIORITIES.eager) {
1313
- preload_code(get_url_path(/** @type {URL} */ (url)));
1457
+ preload_code(/** @type {URL} */ (url).pathname);
1314
1458
  }
1315
1459
  }
1316
1460
  }
@@ -1334,12 +1478,11 @@ export function create_client(app, target) {
1334
1478
  console.warn('The next HMR update will cause the page to reload');
1335
1479
  }
1336
1480
 
1481
+ const status = get_status(error);
1482
+ const message = get_message(error);
1483
+
1337
1484
  return (
1338
- app.hooks.handleError({ error, event }) ??
1339
- /** @type {any} */ ({
1340
- message:
1341
- event.route.id === null && error instanceof NotFound ? 'Not Found' : 'Internal Error'
1342
- })
1485
+ app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
1343
1486
  );
1344
1487
  }
1345
1488
 
@@ -1387,8 +1530,28 @@ export function create_client(app, target) {
1387
1530
  }
1388
1531
  },
1389
1532
 
1390
- goto: (href, opts = {}) => {
1391
- return goto(href, opts, 0);
1533
+ goto: (url, opts = {}) => {
1534
+ url = resolve_url(url);
1535
+
1536
+ // @ts-expect-error
1537
+ if (DEV && opts.state) {
1538
+ // TOOD 3.0 remove
1539
+ throw new Error(
1540
+ 'Passing `state` to `goto` is no longer supported. Use `pushState` and `replaceState` from `$app/navigation` instead.'
1541
+ );
1542
+ }
1543
+
1544
+ if (url.origin !== origin) {
1545
+ return Promise.reject(
1546
+ new Error(
1547
+ DEV
1548
+ ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
1549
+ : 'goto: invalid URL'
1550
+ )
1551
+ );
1552
+ }
1553
+
1554
+ return goto(url, opts, 0);
1392
1555
  },
1393
1556
 
1394
1557
  invalidate: (resource) => {
@@ -1408,17 +1571,89 @@ export function create_client(app, target) {
1408
1571
  },
1409
1572
 
1410
1573
  preload_data: async (href) => {
1411
- const url = new URL(href, get_base_uri(document));
1574
+ const url = resolve_url(href);
1412
1575
  const intent = get_navigation_intent(url, false);
1413
1576
 
1414
1577
  if (!intent) {
1415
1578
  throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
1416
1579
  }
1417
1580
 
1418
- await preload_data(intent);
1581
+ const result = await preload_data(intent);
1582
+ if (result.type === 'redirect') {
1583
+ return {
1584
+ type: result.type,
1585
+ location: result.location
1586
+ };
1587
+ }
1588
+
1589
+ const { status, data } = result.props.page ?? page;
1590
+ return { type: result.type, status, data };
1419
1591
  },
1420
1592
 
1421
- preload_code,
1593
+ preload_code: (pathname) => {
1594
+ if (DEV) {
1595
+ if (!pathname.startsWith(base)) {
1596
+ throw new Error(
1597
+ `pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")`
1598
+ );
1599
+ }
1600
+
1601
+ if (!routes.find((route) => route.exec(get_url_path(pathname)))) {
1602
+ throw new Error(`'${pathname}' did not match any routes`);
1603
+ }
1604
+ }
1605
+
1606
+ return preload_code(pathname);
1607
+ },
1608
+
1609
+ push_state: (url, state) => {
1610
+ if (DEV) {
1611
+ try {
1612
+ devalue.stringify(state);
1613
+ } catch (error) {
1614
+ // @ts-expect-error
1615
+ throw new Error(`Could not serialize state${error.path}`);
1616
+ }
1617
+ }
1618
+
1619
+ const opts = {
1620
+ [HISTORY_INDEX]: (current_history_index += 1),
1621
+ [NAVIGATION_INDEX]: current_navigation_index,
1622
+ [PAGE_URL_KEY]: page.url.href
1623
+ };
1624
+
1625
+ original_push_state.call(history, opts, '', resolve_url(url));
1626
+
1627
+ page = { ...page, state };
1628
+ root.$set({ page });
1629
+
1630
+ states[current_history_index] = state;
1631
+ clear_onward_history(current_history_index, current_navigation_index);
1632
+ },
1633
+
1634
+ replace_state: (url, state) => {
1635
+ if (DEV) {
1636
+ try {
1637
+ devalue.stringify(state);
1638
+ } catch (error) {
1639
+ // @ts-expect-error
1640
+ throw new Error(`Could not serialize state${error.path}`);
1641
+ }
1642
+ }
1643
+
1644
+ const opts = {
1645
+ [HISTORY_INDEX]: current_history_index,
1646
+ [NAVIGATION_INDEX]: current_navigation_index,
1647
+ [PAGE_URL_KEY]: page.url.href
1648
+ };
1649
+
1650
+ original_replace_state.call(history, opts, '', resolve_url(url));
1651
+
1652
+ page = { ...page, state };
1653
+ root.$set({ page });
1654
+
1655
+ states[current_history_index] = state;
1656
+ },
1422
1657
 
1423
1658
  apply_action: async (result) => {
1424
1659
  if (result.type === 'error') {
@@ -1575,7 +1810,7 @@ export function create_client(app, target) {
1575
1810
  // This will ensure the `hashchange` event is fired
1576
1811
  // Removing the hash does a full page navigation in the browser, so make sure a hash is present
1577
1812
  const [nonhash, hash] = url.href.split('#');
1578
- if (hash !== undefined && nonhash === location.href.split('#')[0]) {
1813
+ if (hash !== undefined && nonhash === strip_hash(location)) {
1579
1814
  // If we are trying to navigate to the same hash, we should only
1580
1815
  // attempt to scroll to that element and avoid any history changes.
1581
1816
  // Otherwise, this can cause Firefox to incorrectly assign a null
@@ -1597,21 +1832,16 @@ export function create_client(app, target) {
1597
1832
 
1598
1833
  // hashchange event shouldn't occur if the router is replacing state.
1599
1834
  hash_navigating = false;
1600
- event.preventDefault();
1601
1835
  }
1602
1836
 
1837
+ event.preventDefault();
1838
+
1603
1839
  navigate({
1840
+ type: 'link',
1604
1841
  url,
1605
- scroll: options.noscroll ? scroll_state() : null,
1606
- keepfocus: options.keep_focus ?? false,
1607
- redirect_count: 0,
1608
- details: {
1609
- state: {},
1610
- replaceState: options.replace_state ?? url.href === location.href
1611
- },
1612
- accepted: () => event.preventDefault(),
1613
- blocked: () => event.preventDefault(),
1614
- type: 'link'
1842
+ keepfocus: options.keepfocus,
1843
+ noscroll: options.noscroll,
1844
+ replace_state: options.replace_state ?? url.href === location.href
1615
1845
  });
1616
1846
  });
1617
1847
 
@@ -1638,8 +1868,8 @@ export function create_client(app, target) {
1638
1868
 
1639
1869
  const event_form = /** @type {HTMLFormElement} */ (event.target);
1640
1870
 
1641
- const { keep_focus, noscroll, reload, replace_state } = get_router_options(event_form);
1642
- if (reload) return;
1871
+ const options = get_router_options(event_form);
1872
+ if (options.reload) return;
1643
1873
 
1644
1874
  event.preventDefault();
1645
1875
  event.stopPropagation();
@@ -1655,57 +1885,67 @@ export function create_client(app, target) {
1655
1885
  url.search = new URLSearchParams(data).toString();
1656
1886
 
1657
1887
  navigate({
1888
+ type: 'form',
1658
1889
  url,
1659
- scroll: noscroll ? scroll_state() : null,
1660
- keepfocus: keep_focus ?? false,
1661
- redirect_count: 0,
1662
- details: {
1663
- state: {},
1664
- replaceState: replace_state ?? url.href === location.href
1665
- },
1666
- nav_token: {},
1667
- accepted: () => {},
1668
- blocked: () => {},
1669
- type: 'form'
1890
+ keepfocus: options.keepfocus,
1891
+ noscroll: options.noscroll,
1892
+ replace_state: options.replace_state ?? url.href === location.href
1670
1893
  });
1671
1894
  });
1672
1895
 
1673
1896
  addEventListener('popstate', async (event) => {
1674
- token = {};
1675
- if (event.state?.[INDEX_KEY]) {
1897
+ if (event.state?.[HISTORY_INDEX]) {
1898
+ const history_index = event.state[HISTORY_INDEX];
1899
+ token = {};
1900
+
1676
1901
  // if a popstate-driven navigation is cancelled, we need to counteract it
1677
1902
  // with history.go, which means we end up back here, hence this check
1678
- if (event.state[INDEX_KEY] === current_history_index) return;
1679
-
1680
- const scroll = scroll_positions[event.state[INDEX_KEY]];
1681
- const url = new URL(location.href);
1682
-
1683
- // if the only change is the hash, we don't need to do anything (see https://github.com/sveltejs/kit/pull/10636 for why we need to do `url?.`)...
1684
- if (current.url?.href.split('#')[0] === location.href.split('#')[0]) {
1685
- // ...except update our internal URL tracking and handle scroll
1903
+ if (history_index === current_history_index) return;
1904
+
1905
+ const scroll = scroll_positions[history_index];
1906
+ const state = states[history_index] ?? {};
1907
+ const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
1908
+ const navigation_index = event.state[NAVIGATION_INDEX];
1909
+ const is_hash_change = strip_hash(location) === strip_hash(current.url);
1910
+ const shallow =
1911
+ navigation_index === current_navigation_index && (has_navigated || is_hash_change);
1912
+
1913
+ if (shallow) {
1914
+ // We don't need to navigate, we just need to update scroll and/or state.
1915
+ // This happens with hash links and `pushState`/`replaceState`. The
1916
+ // exception is if we haven't navigated yet, since we could have
1917
+ // got here after a modal navigation then a reload
1686
1918
  update_url(url);
1919
+
1687
1920
  scroll_positions[current_history_index] = scroll_state();
1688
- current_history_index = event.state[INDEX_KEY];
1689
- scrollTo(scroll.x, scroll.y);
1921
+ if (scroll) scrollTo(scroll.x, scroll.y);
1922
+
1923
+ if (state !== page.state) {
1924
+ page = { ...page, state };
1925
+ root.$set({ page });
1926
+ }
1927
+
1928
+ current_history_index = history_index;
1690
1929
  return;
1691
1930
  }
1692
1931
 
1693
- const delta = event.state[INDEX_KEY] - current_history_index;
1932
+ const delta = history_index - current_history_index;
1694
1933
 
1695
1934
  await navigate({
1935
+ type: 'popstate',
1696
1936
  url,
1697
- scroll,
1698
- keepfocus: false,
1699
- redirect_count: 0,
1700
- details: null,
1701
- accepted: () => {
1702
- current_history_index = event.state[INDEX_KEY];
1937
+ popped: {
1938
+ state,
1939
+ scroll,
1940
+ delta
1941
+ },
1942
+ accept: () => {
1943
+ current_history_index = history_index;
1944
+ current_navigation_index = navigation_index;
1703
1945
  },
1704
- blocked: () => {
1946
+ block: () => {
1705
1947
  history.go(-delta);
1706
1948
  },
1707
- type: 'popstate',
1708
- delta,
1709
1949
  nav_token: token
1710
1950
  });
1711
1951
  } else {
@@ -1724,8 +1964,13 @@ export function create_client(app, target) {
1724
1964
  // we need to update history, otherwise we have to leave it alone
1725
1965
  if (hash_navigating) {
1726
1966
  hash_navigating = false;
1727
- history.replaceState(
1728
- { ...history.state, [INDEX_KEY]: ++current_history_index },
1967
+ original_replace_state.call(
1968
+ history,
1969
+ {
1970
+ ...history.state,
1971
+ [HISTORY_INDEX]: ++current_history_index,
1972
+ [NAVIGATION_INDEX]: current_navigation_index
1973
+ },
1729
1974
  '',
1730
1975
  location.href
1731
1976
  );
@@ -1839,13 +2084,17 @@ export function create_client(app, target) {
1839
2084
  }
1840
2085
 
1841
2086
  result = await load_root_error_page({
1842
- status: error instanceof HttpError ? error.status : 500,
2087
+ status: get_status(error),
1843
2088
  error: await handle_error(error, { url, params, route }),
1844
2089
  url,
1845
2090
  route
1846
2091
  });
1847
2092
  }
1848
2093
 
2094
+ if (result.props.page) {
2095
+ result.props.page.state = {};
2096
+ }
2097
+
1849
2098
  initialize(result);
1850
2099
  }
1851
2100
  };
@@ -1966,7 +2215,8 @@ function deserialize_uses(uses) {
1966
2215
  params: new Set(uses?.params ?? []),
1967
2216
  parent: !!uses?.parent,
1968
2217
  route: !!uses?.route,
1969
- url: !!uses?.url
2218
+ url: !!uses?.url,
2219
+ search_params: new Set(uses?.search_params ?? [])
1970
2220
  };
1971
2221
  }
1972
2222