@sveltejs/kit 3.0.0-next.12 → 3.0.0-next.14

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 (77) hide show
  1. package/package.json +11 -11
  2. package/src/cli.js +2 -2
  3. package/src/core/adapt/builder.js +8 -5
  4. package/src/core/generate_manifest/index.js +6 -0
  5. package/src/core/postbuild/entities.js +8 -2
  6. package/src/core/postbuild/fallback.js +2 -1
  7. package/src/core/postbuild/prerender.js +15 -6
  8. package/src/core/sync/create_manifest_data/conflict.js +1 -1
  9. package/src/core/sync/create_manifest_data/index.js +33 -2
  10. package/src/core/sync/write_app_types.js +73 -29
  11. package/src/core/sync/write_tsconfig/index.js +15 -2
  12. package/src/core/sync/write_types/index.js +5 -2
  13. package/src/exports/index.js +23 -12
  14. package/src/exports/internal/shared.js +4 -12
  15. package/src/exports/node/index.js +2 -7
  16. package/src/exports/public.d.ts +61 -12
  17. package/src/exports/vite/build/remote.js +10 -12
  18. package/src/exports/vite/dev/index.js +32 -19
  19. package/src/exports/vite/index.js +162 -133
  20. package/src/exports/vite/utils.js +1 -7
  21. package/src/runtime/app/server/remote/form.js +9 -1
  22. package/src/runtime/app/server/remote/prerender.js +13 -9
  23. package/src/runtime/app/server/remote/query.js +0 -6
  24. package/src/runtime/app/server/remote/requested.js +4 -4
  25. package/src/runtime/app/state/client.js +3 -0
  26. package/src/runtime/app/state/index.js +2 -2
  27. package/src/runtime/app/state/server.js +3 -0
  28. package/src/runtime/client/client.js +731 -342
  29. package/src/runtime/client/constants.js +2 -6
  30. package/src/runtime/client/fetcher.js +24 -25
  31. package/src/runtime/client/remote-functions/form.svelte.js +42 -14
  32. package/src/runtime/client/remote-functions/prerender.svelte.js +2 -2
  33. package/src/runtime/client/remote-functions/query/index.js +1 -1
  34. package/src/runtime/client/remote-functions/query/instance.svelte.js +2 -2
  35. package/src/runtime/client/remote-functions/query-batch.svelte.js +2 -2
  36. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +2 -2
  37. package/src/runtime/client/remote-functions/query-live/iterator.js +10 -8
  38. package/src/runtime/client/remote-functions/query-live/proxy.js +0 -10
  39. package/src/runtime/client/remote-functions/shared.svelte.js +12 -11
  40. package/src/runtime/client/state.svelte.js +10 -22
  41. package/src/runtime/client/types.d.ts +1 -2
  42. package/src/runtime/client/utils.js +22 -14
  43. package/src/runtime/components/root.svelte +4 -14
  44. package/src/runtime/form-utils.js +4 -6
  45. package/src/runtime/pathname.js +37 -0
  46. package/src/runtime/props.svelte.js +71 -0
  47. package/src/runtime/server/fetch.js +9 -14
  48. package/src/runtime/server/index.js +10 -24
  49. package/src/runtime/server/page/crypto.js +2 -2
  50. package/src/runtime/server/page/csp.js +88 -104
  51. package/src/runtime/server/page/index.js +1 -2
  52. package/src/runtime/server/page/load_data.js +16 -33
  53. package/src/runtime/server/page/render.js +46 -25
  54. package/src/runtime/server/page/respond_with_error.js +1 -3
  55. package/src/runtime/server/page/serialize_data.js +2 -13
  56. package/src/runtime/server/page/server_routing.js +58 -9
  57. package/src/runtime/server/respond.js +20 -29
  58. package/src/runtime/server/state.js +43 -0
  59. package/src/runtime/shared.js +4 -2
  60. package/src/runtime/utils.js +3 -0
  61. package/src/types/ambient-private.d.ts +2 -2
  62. package/src/types/ambient.d.ts +59 -8
  63. package/src/types/global-private.d.ts +1 -1
  64. package/src/types/internal.d.ts +4 -8
  65. package/src/types/private.d.ts +3 -12
  66. package/src/utils/hash.js +21 -0
  67. package/src/utils/http.js +8 -7
  68. package/src/utils/import.js +2 -1
  69. package/src/utils/params.js +1 -1
  70. package/src/utils/regex.js +9 -0
  71. package/src/utils/routing.js +52 -27
  72. package/src/utils/url.js +1 -0
  73. package/src/version.js +1 -1
  74. package/types/index.d.ts +165 -66
  75. package/types/index.d.ts.map +2 -1
  76. package/src/exports/node/polyfills.js +0 -30
  77. package/src/runtime/types.d.ts +0 -8
@@ -1,12 +1,11 @@
1
1
  /** @import { RouteId } from '$app/types' */
2
2
  /** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */
3
- /** @import { NavigationIntent } from './types.js' */
4
- /** @import { RenderNode } from '../types.js' */
3
+ /** @import { NavigationFinished, NavigationIntent } from './types.js' */
5
4
  /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */
6
5
  /** @import { Query } from './remote-functions/query/instance.svelte.js' */
7
6
  /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */
8
7
  import { BROWSER, DEV } from 'esm-env';
9
- import * as svelte from 'svelte';
8
+ import { settled, tick, fork, onMount, hydrate, mount } from 'svelte';
10
9
  import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
11
10
  import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js';
12
11
  import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
@@ -22,16 +21,13 @@ import {
22
21
  scroll_state,
23
22
  load_css
24
23
  } from './utils.js';
25
- import { base, set_match_implementation } from '$app/paths/internal/client';
24
+ import { base, app_dir, set_match_implementation } from '$app/paths/internal/client';
26
25
  import * as devalue from 'devalue';
27
26
  import {
28
- HISTORY_INDEX,
29
- NAVIGATION_INDEX,
27
+ HISTORY_INFO_KEY,
28
+ HISTORY_METADATA_KEY,
30
29
  PRELOAD_PRIORITIES,
31
- SCROLL_KEY,
32
- STATES_KEY,
33
- SNAPSHOT_KEY,
34
- PAGE_URL_KEY
30
+ SNAPSHOT_KEY
35
31
  } from './constants.js';
36
32
  import { validate_page_exports } from '../../utils/exports.js';
37
33
  import { noop } from '../../utils/functions.js';
@@ -43,28 +39,33 @@ import {
43
39
  validate_load_response
44
40
  } from '../shared.js';
45
41
  import { get_message, get_status } from '../../utils/error.js';
46
- import { page, update, navigating, updated, notify_version } from './state.svelte.js';
42
+ import { page, navigating, updated, notify_version } from './state.svelte.js';
47
43
  import { payload } from './payload.js';
48
- import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
44
+ import {
45
+ add_data_suffix,
46
+ add_resolution_suffix,
47
+ route_id_resolution_pathname
48
+ } from '../pathname.js';
49
49
  import { noop_span } from '../telemetry/noop.js';
50
50
  import { read_ndjson } from './ndjson.js';
51
- import RootModern from '../components/root.svelte';
52
- import { asClassComponent } from 'svelte/legacy';
51
+ import Root from '../components/root.svelte';
52
+ import { Props, RenderNode } from '../props.svelte.js';
53
53
 
54
- const Root = asClassComponent(RootModern);
54
+ /**
55
+ * @typedef {{
56
+ * historyIndex: number;
57
+ * navigationIndex: number;
58
+ * pageUrl?: string;
59
+ * state: Record<string, any>;
60
+ * persistState: boolean;
61
+ * resetIndex: number;
62
+ * }} HistoryMetadata
63
+ */
55
64
 
56
65
  export { load_css };
57
66
  const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
58
67
 
59
68
  let errored = false;
60
- /**
61
- * Set via transformError, reset and read at the end of navigate.
62
- * Necessary because a navigation might succeed loading but during rendering
63
- * an error occurs, at which point the navigation result needs to be overridden with the error result.
64
- * TODO this is all very hacky, rethink for SvelteKit 3 where we can assume Svelte 5 and do an overhaul of client.js
65
- * @type {{ error: App.Error, status: number } | null}
66
- */
67
- let rendering_error = null;
68
69
 
69
70
  /**
70
71
  * `reset` functions for `<svelte:boundary>`s in the generated root that have
@@ -73,19 +74,18 @@ let rendering_error = null;
73
74
  * navigation away from a render error would leave the stale `+error.svelte`
74
75
  * mounted. The boundary's `onerror` populates this array; `navigate` drains it
75
76
  * after applying the new props. See sveltejs/kit#15694.
76
- * @type {Array<(() => void) | undefined>}
77
+ * @type {Set<() => void>}
77
78
  */
78
- const resetters = [];
79
+ const resetters = new Set();
79
80
 
80
- // We track the scroll position associated with each history entry in sessionStorage,
81
+ // We track information associated with each history entry in sessionStorage,
81
82
  // rather than on history.state itself, because when navigation is driven by
82
- // popstate it's too late to update the scroll position associated with the
83
+ // popstate it's too late to access the options or update the focus position associated with the
83
84
  // state we're navigating from
84
85
  /**
85
- * history index -> { x, y }
86
- * @type {Record<number, { x: number; y: number }>}
86
+ * @type {Record<number, { scroll?: { x: number; y: number }; resetIndex?: number }>}
87
87
  */
88
- const scroll_positions = storage.get(SCROLL_KEY) ?? {};
88
+ const history_info = storage.get(HISTORY_INFO_KEY) ?? {};
89
89
 
90
90
  /**
91
91
  * navigation index -> any
@@ -93,14 +93,14 @@ const scroll_positions = storage.get(SCROLL_KEY) ?? {};
93
93
  */
94
94
  const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
95
95
 
96
- /**
97
- * @deprecated this is a temporary measure to avoid a regression, replace with nested `RenderNode` classes
98
- */
99
- let current_tree = /** @type {RenderNode} */ ({});
96
+ /** @type {Props} */
97
+ let props;
100
98
 
101
99
  if (DEV && BROWSER) {
102
100
  let warned = false;
103
101
 
102
+ const current_module_url = import.meta.url.split('?')[0]; // remove query params that vite adds to the URL when it is loaded from node_modules
103
+
104
104
  const warn = () => {
105
105
  if (warned) return;
106
106
 
@@ -113,14 +113,15 @@ if (DEV && BROWSER) {
113
113
  // skip over `warn` and the place where `warn` was called
114
114
  const frame = stack[2];
115
115
 
116
- // ignore calls that happen inside dependencies, including SvelteKit.
116
+ // Ignore calls that happen inside dependencies, including SvelteKit.
117
+ // The second condition is only relevant when developing SvelteKit and running it, as there's no node_modules in the stack then (but we still do it to not get repeatedly confused)
117
118
  // `frame` can be falsy if we came from an anonymous function
118
- if (frame?.includes('node_modules')) return;
119
+ if (frame?.includes('node_modules') || frame?.includes(current_module_url)) return;
119
120
 
120
121
  warned = true;
121
122
 
122
123
  console.warn(
123
- "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."
124
+ "Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use `goto(...)` from `$app/navigation` instead."
124
125
  );
125
126
  };
126
127
 
@@ -138,8 +139,60 @@ if (DEV && BROWSER) {
138
139
  }
139
140
 
140
141
  /** @param {number} index */
141
- function update_scroll_positions(index) {
142
- scroll_positions[index] = scroll_state();
142
+ function capture_scroll(index) {
143
+ history_info[index].scroll = scroll_state();
144
+ }
145
+
146
+ /**
147
+ * @param {number} index
148
+ * @param {Pick<HistoryMetadata, 'resetIndex'>} options
149
+ */
150
+ function set_history_options(index, options) {
151
+ history_info[index] = {
152
+ ...history_info[index],
153
+ resetIndex: options.resetIndex
154
+ };
155
+ }
156
+
157
+ /** @param {boolean} reset */
158
+ function blur_active_element(reset) {
159
+ if (
160
+ reset &&
161
+ document.activeElement instanceof HTMLElement &&
162
+ document.activeElement !== document.body
163
+ ) {
164
+ document.activeElement.blur();
165
+ }
166
+ }
167
+
168
+ /**
169
+ * @param {URL} url
170
+ * @param {{ x: number; y: number } | null | undefined} scroll
171
+ * @param {boolean} reset
172
+ * @param {Element | null} active_element
173
+ */
174
+ function reset_scroll_and_focus(url, scroll, reset, active_element) {
175
+ /** @type {Element | null} */
176
+ let deep_linked = null;
177
+
178
+ if (autoscroll) {
179
+ if (scroll) {
180
+ scrollTo(scroll.x, scroll.y);
181
+ } else if ((deep_linked = get_hash_element(url))) {
182
+ deep_linked.scrollIntoView();
183
+ } else {
184
+ scrollTo(0, 0);
185
+ }
186
+ }
187
+
188
+ const changed_focus =
189
+ document.activeElement !== active_element && document.activeElement !== document.body;
190
+
191
+ if (reset && !changed_focus) {
192
+ reset_focus(url, !deep_linked);
193
+ }
194
+
195
+ autoscroll = true;
143
196
  }
144
197
 
145
198
  /**
@@ -150,8 +203,8 @@ function clear_onward_history(current_history_index, current_navigation_index) {
150
203
  // if we navigated back, then pushed a new state, we can
151
204
  // release memory by pruning the scroll/snapshot lookup
152
205
  let i = current_history_index + 1;
153
- while (scroll_positions[i]) {
154
- delete scroll_positions[i];
206
+ while (history_info[i]) {
207
+ delete history_info[i];
155
208
  i += 1;
156
209
  }
157
210
 
@@ -225,14 +278,6 @@ export const prerender_responses = {};
225
278
  /** @type {Array<((url: URL) => boolean)>} */
226
279
  const invalidated = [];
227
280
 
228
- /**
229
- * An array of the `+layout.svelte` and `+page.svelte` component instances
230
- * that currently live on the page — used for capturing and restoring snapshots.
231
- * It's updated/manipulated through `bind:this` in `Root.svelte`.
232
- * @type {import('svelte').SvelteComponent[]}
233
- */
234
- const components = [];
235
-
236
281
  /** @type {{id: string, token: {}, promise: Promise<import('./types.js').NavigationResult>, fork: Promise<import('svelte').Fork | null> | null} | null} */
237
282
  let load_cache = null;
238
283
 
@@ -253,6 +298,35 @@ function discard_load_cache() {
253
298
  */
254
299
  const reroute_cache = new Map();
255
300
 
301
+ /**
302
+ * Sentinel for a route that exists but has no code to preload, i.e. a `+server.js` with no
303
+ * `+page`. Cached like a parsed route so that repeated `preloadCode(id)` calls for the same
304
+ * id don't re-request it.
305
+ */
306
+ const ENDPOINT_ONLY = Symbol('endpoint only');
307
+
308
+ /**
309
+ * Cache of route ID -> parsed route for server-side route resolution.
310
+ * Populated whenever a server route resolution occurs (`match`, link preloading,
311
+ * hydration), so that `preloadCode(id)` doesn't need an extra server round trip.
312
+ * Lives until full page reload.
313
+ * @type {Map<string, import('types').CSRRoute | typeof ENDPOINT_ONLY>}
314
+ */
315
+ const route_id_cache = new Map();
316
+
317
+ /**
318
+ * Parse a server-provided route and record it in `route_id_cache`, so that a subsequent
319
+ * `preloadCode(id)` for the same route doesn't need another server round trip. Always use
320
+ * this rather than calling `parse_server_route` directly.
321
+ * @param {import('types').CSRRouteServer} server_route
322
+ * @returns {import('types').CSRRoute}
323
+ */
324
+ function parse_and_cache_server_route(server_route) {
325
+ const route = parse_server_route(server_route, app.nodes);
326
+ route_id_cache.set(route.id, route);
327
+ return route;
328
+ }
329
+
256
330
  /**
257
331
  * Note on before_navigate_callbacks, on_navigate_callbacks and after_navigate_callbacks:
258
332
  * do not re-assign as some closures keep references to these Sets
@@ -282,21 +356,30 @@ let started = false;
282
356
  let autoscroll = true;
283
357
  let updating = false;
284
358
  let is_navigating = false;
285
- let hash_navigating = false;
359
+ /** @type {HistoryMetadata | null} */
360
+ let hash_navigating = null;
286
361
  /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
287
362
  let has_navigated = false;
288
363
 
289
364
  let force_invalidation = false;
290
365
 
291
- /** @type {import('svelte').SvelteComponent} */
292
- let root;
293
-
294
366
  /** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
295
367
  let current_history_index;
296
368
 
297
369
  /** @type {number} */
298
370
  let current_navigation_index;
299
371
 
372
+ /** @type {number} */
373
+ let current_reset_index;
374
+
375
+ /**
376
+ * @param {any} [state]
377
+ * @returns {HistoryMetadata | undefined}
378
+ */
379
+ function get_history_metadata(state = history.state) {
380
+ return state?.[HISTORY_METADATA_KEY];
381
+ }
382
+
300
383
  /** @type {{}} Token for the latest navigation. Updated on new navigations */
301
384
  let navigation_token;
302
385
 
@@ -319,7 +402,7 @@ let invalidation_token;
319
402
  const preload_tokens = new Set();
320
403
 
321
404
  /** @type {Promise<void> | null} */
322
- export let pending_invalidate;
405
+ let pending_invalidate;
323
406
 
324
407
  /**
325
408
  * @type {Map<string, Map<string, CacheEntry<Query<any>>>>}
@@ -350,12 +433,33 @@ set_match_implementation(async (url) => {
350
433
  return null;
351
434
  });
352
435
 
436
+ let embedded_start = Promise.resolve();
437
+
438
+ /**
439
+ * TODO this indirection is a grotesque workaround for the fact that multiple apps
440
+ * can operate on the same shared mutable state. This is fundamentally unsound,
441
+ * and the `start`/`_start` distinction does not fix it, but it does get the
442
+ * tests passing. We need to rethink this whole thing but not right now
443
+ * @param {import('./types.js').SvelteKitApp} _app
444
+ * @param {HTMLElement} _target
445
+ * @param {Parameters<typeof _hydrate>[1]} [data]
446
+ */
447
+ export function start(_app, _target, data) {
448
+ if (__SVELTEKIT_EMBEDDED__) {
449
+ const start_promise = embedded_start.then(() => _start(_app, _target, data));
450
+ embedded_start = start_promise.catch(noop);
451
+ return start_promise;
452
+ }
453
+
454
+ return _start(_app, _target, data);
455
+ }
456
+
353
457
  /**
354
458
  * @param {import('./types.js').SvelteKitApp} _app
355
459
  * @param {HTMLElement} _target
356
- * @param {Parameters<typeof _hydrate>[1]} [hydrate]
460
+ * @param {Parameters<typeof _hydrate>[1]} [data]
357
461
  */
358
- export async function start(_app, _target, hydrate) {
462
+ async function _start(_app, _target, data) {
359
463
  if (DEV && _target === document.body) {
360
464
  console.warn(
361
465
  'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>'
@@ -393,31 +497,56 @@ export async function start(_app, _target, hydrate) {
393
497
  // connectivity errors after initialisation don't nuke the app
394
498
  default_layout_loader = _app.nodes[0];
395
499
  default_error_loader = _app.nodes[1];
396
- void default_layout_loader();
397
- void default_error_loader();
398
500
 
399
- current_history_index = history.state?.[HISTORY_INDEX];
400
- current_navigation_index = history.state?.[NAVIGATION_INDEX];
501
+ const [root_layout, root_error] = await Promise.all([
502
+ default_layout_loader(),
503
+ default_error_loader()
504
+ ]);
505
+
506
+ const tree = new RenderNode(root_layout.component, root_error.component);
507
+
508
+ props = new Props({
509
+ page,
510
+ tree,
511
+ form: undefined,
512
+ error: undefined,
513
+ onerror: (_, reset) => resetters.add(reset)
514
+ });
515
+
516
+ const history_metadata = get_history_metadata();
517
+ current_history_index = history_metadata?.historyIndex ?? 0;
518
+ current_navigation_index = history_metadata?.navigationIndex ?? 0;
519
+ current_reset_index = history_metadata?.resetIndex ?? 0;
401
520
 
402
521
  if (!current_history_index) {
403
522
  // we use Date.now() as an offset so that cross-document navigations
404
523
  // within the app don't result in data loss
405
- current_history_index = current_navigation_index = Date.now();
524
+ current_history_index = current_navigation_index = current_reset_index = Date.now();
406
525
 
407
526
  // create initial history entry, so we can return here
408
527
  history.replaceState(
409
528
  {
410
529
  ...history.state,
411
- [HISTORY_INDEX]: current_history_index,
412
- [NAVIGATION_INDEX]: current_navigation_index
530
+ [HISTORY_METADATA_KEY]: {
531
+ historyIndex: current_history_index,
532
+ navigationIndex: current_navigation_index,
533
+ state: {},
534
+ persistState: false,
535
+ resetIndex: current_history_index
536
+ }
413
537
  },
414
538
  ''
415
539
  );
416
540
  }
417
541
 
542
+ set_history_options(
543
+ current_history_index,
544
+ /** @type {HistoryMetadata} */ (get_history_metadata())
545
+ );
546
+
418
547
  // if we reload the page, or Cmd-Shift-T back to it,
419
548
  // recover scroll position
420
- const scroll = scroll_positions[current_history_index];
549
+ const scroll = history_info[current_history_index]?.scroll;
421
550
  function restore_scroll() {
422
551
  if (scroll) {
423
552
  history.scrollRestoration = 'manual';
@@ -425,15 +554,17 @@ export async function start(_app, _target, hydrate) {
425
554
  }
426
555
  }
427
556
 
428
- if (hydrate) {
557
+ if (data) {
429
558
  restore_scroll();
430
559
 
431
- await _hydrate(target, hydrate);
560
+ await _hydrate(target, data);
432
561
  } else {
433
562
  await navigate({
434
563
  type: 'enter',
435
564
  url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href),
436
- replace_state: true
565
+ replace_state: true,
566
+ state: history_metadata?.persistState ? history_metadata.state : {},
567
+ persist_state: history_metadata?.persistState ?? false
437
568
  });
438
569
 
439
570
  restore_scroll();
@@ -482,6 +613,7 @@ async function _invalidate(reset_page_state = true) {
482
613
  }
483
614
 
484
615
  const prev_state = page.state;
616
+ const prev_shallow = page.shallow;
485
617
  const navigation_result = intent && (await load_route(intent));
486
618
  if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
487
619
  return;
@@ -490,7 +622,7 @@ async function _invalidate(reset_page_state = true) {
490
622
  if (navigation_result.type === 'redirect') {
491
623
  return _goto(
492
624
  new URL(navigation_result.location, current.url).href,
493
- { replaceState: true },
625
+ { replace: true },
494
626
  1,
495
627
  token
496
628
  );
@@ -506,11 +638,10 @@ async function _invalidate(reset_page_state = true) {
506
638
  if (!reset_page_state) {
507
639
  navigation_result.props.page.state = prev_state;
508
640
  }
509
- update(navigation_result.props.page);
510
- current_tree = navigation_result.props.tree;
641
+ navigation_result.props.page.shallow = prev_shallow;
642
+ apply_navigation_result(navigation_result);
511
643
  current = { ...navigation_result.state, nav: current.nav };
512
644
  reset_invalidation();
513
- root.$set(navigation_result.props);
514
645
 
515
646
  // only wait for promises that are connected to queries that still exist
516
647
  /** @type {Promise<any>[]} */
@@ -541,21 +672,21 @@ function reset_invalidation() {
541
672
 
542
673
  /** @param {number} index */
543
674
  function capture_snapshot(index) {
544
- if (components.some((c) => c?.snapshot)) {
545
- snapshots[index] = components.map((c) => c?.snapshot?.capture());
675
+ if (props.components.some((c) => c?.snapshot)) {
676
+ snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
546
677
  }
547
678
  }
548
679
 
549
680
  /** @param {number} index */
550
681
  function restore_snapshot(index) {
551
682
  snapshots[index]?.forEach((value, i) => {
552
- components[i]?.snapshot?.restore(value);
683
+ props.components[i]?.snapshot?.restore(value);
553
684
  });
554
685
  }
555
686
 
556
687
  function persist_state() {
557
- update_scroll_positions(current_history_index);
558
- storage.set(SCROLL_KEY, scroll_positions);
688
+ capture_scroll(current_history_index);
689
+ storage.set(HISTORY_INFO_KEY, history_info);
559
690
 
560
691
  capture_snapshot(current_navigation_index);
561
692
  storage.set(SNAPSHOT_KEY, snapshots);
@@ -563,13 +694,13 @@ function persist_state() {
563
694
 
564
695
  /**
565
696
  * @param {string | URL} url
566
- * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
567
- * @param {number} redirect_count
697
+ * @param {{ type?: import('@sveltejs/kit').NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event }} [options]
698
+ * @param {number} [redirect_count]
568
699
  * @param {{}} [nav_token]
569
700
  * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
570
701
  * @returns {Promise<void>}
571
702
  */
572
- export async function _goto(url, options, redirect_count, nav_token, intent) {
703
+ export async function _goto(url, options = {}, redirect_count = 0, nav_token = {}, intent) {
573
704
  /** @type {Set<string>} */
574
705
  let query_keys;
575
706
  /** @type {Set<string>} */
@@ -582,12 +713,13 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
582
713
  }
583
714
 
584
715
  await navigate({
585
- type: 'goto',
716
+ type: options.type ?? 'goto',
586
717
  url: resolve_url(url),
587
- keepfocus: options.keepFocus,
588
- noscroll: options.noScroll,
589
- replace_state: options.replaceState,
718
+ reset: options.reset,
719
+ replace_state: options.replace,
590
720
  state: options.state,
721
+ persist_state: options.persistState,
722
+ event: options.event,
591
723
  redirect_count,
592
724
  nav_token,
593
725
  intent,
@@ -621,9 +753,8 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
621
753
  if (options.refreshAll) {
622
754
  // TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
623
755
  // when a query in a layout that still exists after page change is refreshed earlier than this
624
- void svelte
625
- .tick()
626
- .then(svelte.tick)
756
+ void tick()
757
+ .then(tick)
627
758
  .then(() => {
628
759
  for (const [id, entries] of query_map) {
629
760
  for (const [payload, { resource }] of entries) {
@@ -665,7 +796,7 @@ async function _preload_data(intent) {
665
796
 
666
797
  load_cache.promise.catch(discard_load_cache);
667
798
 
668
- if (__SVELTEKIT_FORK_PRELOADS__ && svelte.fork) {
799
+ if (__SVELTEKIT_FORK_PRELOADS__) {
669
800
  const lc = load_cache;
670
801
 
671
802
  lc.fork = lc.promise.then((result) => {
@@ -673,10 +804,8 @@ async function _preload_data(intent) {
673
804
  // resolve, bail rather than creating an orphan fork
674
805
  if (lc === load_cache && result.type === 'loaded') {
675
806
  try {
676
- return svelte.fork(() => {
677
- root.$set(result.props);
678
- update(result.props.page);
679
- current_tree = result.props.tree;
807
+ return fork(() => {
808
+ apply_navigation_result(result);
680
809
  });
681
810
  } catch {
682
811
  // if it errors, it's because the experimental flag isn't enabled in Svelte
@@ -691,6 +820,53 @@ async function _preload_data(intent) {
691
820
  return load_cache.promise;
692
821
  }
693
822
 
823
+ /**
824
+ * Fetch and parse the route with the given ID from the server-side route resolution endpoint.
825
+ * Returns `ENDPOINT_ONLY` if the route exists but has no code to preload, or `undefined` if
826
+ * there is no such route. Only used when `router.resolution === 'server'`.
827
+ * @param {string} id
828
+ * @returns {Promise<import('types').CSRRoute | typeof ENDPOINT_ONLY | undefined>}
829
+ */
830
+ async function load_route_by_id(id) {
831
+ /** @type {{ route?: import('types').CSRRouteServer, endpoint_only?: boolean }} */
832
+ let module;
833
+
834
+ try {
835
+ module = await import(
836
+ /* @vite-ignore */
837
+ base + route_id_resolution_pathname(app_dir, id)
838
+ );
839
+ } catch {
840
+ // if there's no module at that path the response is a 404 (or, on a static
841
+ // host, fallback HTML with the wrong MIME type) and the import rejects —
842
+ // treat it the same as an unknown route rather than surfacing a cryptic error
843
+ return;
844
+ }
845
+
846
+ if (module.endpoint_only) {
847
+ // The route exists, it just has no code to preload.
848
+ route_id_cache.set(id, ENDPOINT_ONLY);
849
+ return ENDPOINT_ONLY;
850
+ }
851
+
852
+ if (!module.route) return;
853
+
854
+ return parse_and_cache_server_route(module.route);
855
+ }
856
+
857
+ /**
858
+ * Import the modules for a route's layout and leaf nodes, without running `load` functions.
859
+ * @param {import('types').CSRRoute} route
860
+ * @returns {Promise<void>}
861
+ */
862
+ async function load_route_nodes(route) {
863
+ await Promise.all(
864
+ /** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
865
+ [...route.layouts, route.leaf].filter(Boolean)
866
+ ).map(([, node_loader]) => node_loader())
867
+ );
868
+ }
869
+
694
870
  /**
695
871
  * @param {URL} url
696
872
  * @returns {Promise<void>}
@@ -699,20 +875,16 @@ async function _preload_code(url) {
699
875
  const route = (await get_navigation_intent(url, false))?.route;
700
876
 
701
877
  if (route) {
702
- await Promise.all(
703
- /** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
704
- [...route.layouts, route.leaf].filter(Boolean)
705
- ).map((load) => load[1]())
706
- );
878
+ await load_route_nodes(route);
707
879
  }
708
880
  }
709
881
 
710
882
  /**
711
883
  * @param {import('./types.js').NavigationFinished} result
712
884
  * @param {HTMLElement} target
713
- * @param {boolean} hydrate
885
+ * @param {boolean} should_hydrate
714
886
  */
715
- async function initialize(result, target, hydrate) {
887
+ async function initialize(result, target, should_hydrate) {
716
888
  if (__SVELTEKIT_DEV__ && result.state.error && document.querySelector('vite-error-overlay'))
717
889
  return;
718
890
 
@@ -734,21 +906,18 @@ async function initialize(result, target, hydrate) {
734
906
  if (style) style.remove();
735
907
  }
736
908
 
737
- update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page));
738
- current_tree = result.props.tree;
909
+ apply_navigation_result(result);
739
910
 
740
- // TODO: use mount()
741
- root = new Root({
911
+ // TODO treeshake `hydrate` in csr mode
912
+ const render = should_hydrate ? hydrate : mount;
913
+
914
+ render(Root, {
742
915
  target,
743
- props: { ...result.props, components, resetters },
744
- hydrate,
745
- // Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync
746
- sync: false,
916
+ props,
747
917
  transformError: /** @param {unknown} e */ async (e) => {
748
918
  const error = await handle_error(e, current.nav);
749
- rendering_error = { error, status: error.status };
750
919
  page.error = error;
751
- page.status = rendering_error.status;
920
+ page.status = error.status;
752
921
  return error;
753
922
  }
754
923
  });
@@ -757,16 +926,17 @@ async function initialize(result, target, hydrate) {
757
926
  // which causes component script blocks to run asynchronously
758
927
  void (await Promise.resolve());
759
928
 
760
- if (hydrate) {
929
+ if (should_hydrate) {
761
930
  /** @type {import('@sveltejs/kit').AfterNavigate} */
762
931
  const navigation = {
763
932
  from: null,
764
933
  to: {
765
934
  ...nav,
766
- scroll: scroll_positions[current_history_index] ?? scroll_state()
935
+ scroll: history_info[current_history_index]?.scroll ?? scroll_state()
767
936
  },
768
937
  willUnload: false,
769
938
  type: 'enter',
939
+ shallow: false,
770
940
  complete: Promise.resolve()
771
941
  };
772
942
 
@@ -848,7 +1018,7 @@ async function get_navigation_result_from_branch({
848
1018
  let current_node = result.props.tree;
849
1019
 
850
1020
  /** @type {RenderNode | undefined} */
851
- let previous_node = current_tree;
1021
+ let previous_node = props.tree;
852
1022
 
853
1023
  for (let i = 0; i < branch.length; i += 1) {
854
1024
  const node = branch[i];
@@ -856,11 +1026,6 @@ async function get_navigation_result_from_branch({
856
1026
 
857
1027
  if (!node) continue;
858
1028
 
859
- const error_loader = errors?.slice(0, i + 1).findLast((x) => x) ?? default_error_loader;
860
-
861
- current_node.error = (await error_loader()).component;
862
- current_node.component = node.node.component;
863
-
864
1029
  if (
865
1030
  // if an ancestor node in this path changed, `data_changed` is already true and the
866
1031
  // accumulated `data` differs from the previous render, so we must re-merge
@@ -878,10 +1043,24 @@ async function get_navigation_result_from_branch({
878
1043
  data = current_node.data;
879
1044
 
880
1045
  if (i < branch.length - 1) {
881
- current_node.child = /** @type {import('../types.js').RenderNode} */ ({});
882
- current_node = current_node.child;
1046
+ let next_index = i + 1;
1047
+ while (next_index < branch.length && !branch[next_index]) {
1048
+ next_index += 1;
1049
+ }
1050
+
1051
+ const next = branch[next_index];
1052
+
1053
+ if (next) {
1054
+ const error_loader =
1055
+ errors?.slice(0, next_index + 1).findLast((x) => x) ?? default_error_loader;
1056
+
1057
+ current_node = current_node.child = new RenderNode(
1058
+ next.node.component,
1059
+ (await error_loader()).component
1060
+ );
883
1061
 
884
- previous_node = previous_node?.child;
1062
+ previous_node = previous_node?.child;
1063
+ }
885
1064
  }
886
1065
  }
887
1066
 
@@ -900,6 +1079,7 @@ async function get_navigation_result_from_branch({
900
1079
  id: route?.id ?? null
901
1080
  },
902
1081
  state: {},
1082
+ shallow: null,
903
1083
  status: status ?? error?.status ?? 200,
904
1084
  url: new URL(url),
905
1085
  form: form ?? null,
@@ -1298,7 +1478,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1298
1478
 
1299
1479
  if (server_data_node?.type === 'error') {
1300
1480
  // rethrow and catch below
1301
- throw new HttpError(server_data_node.error.status, server_data_node.error);
1481
+ throw new HttpError(server_data_node.error);
1302
1482
  }
1303
1483
 
1304
1484
  return load_node({
@@ -1504,7 +1684,7 @@ async function load_root_error_page({ error, url, route }) {
1504
1684
  // client-side navigation if the root layout loader throws a redirect while
1505
1685
  // rendering the default error page
1506
1686
  if (error instanceof Redirect) {
1507
- await _goto(new URL(error.location, location.href), {}, 0);
1687
+ await _goto(new URL(error.location, location.href));
1508
1688
  return;
1509
1689
  }
1510
1690
 
@@ -1626,7 +1806,7 @@ export async function get_navigation_intent(url, invalidating) {
1626
1806
  return {
1627
1807
  id: get_page_key(url),
1628
1808
  invalidating,
1629
- route: parse_server_route(route, app.nodes),
1809
+ route: parse_and_cache_server_route(route),
1630
1810
  params,
1631
1811
  url
1632
1812
  };
@@ -1655,12 +1835,14 @@ function get_page_key(url) {
1655
1835
  * delta?: number;
1656
1836
  * event?: PopStateEvent | MouseEvent;
1657
1837
  * scroll?: { x: number, y: number };
1838
+ * shallow?: boolean;
1839
+ * target?: { params: Record<string, string> | null; route: { id: string } | null; url: URL };
1658
1840
  * }} opts
1659
1841
  */
1660
- function _before_navigate({ url, type, intent, delta, event, scroll }) {
1842
+ function _before_navigate({ url, type, intent, delta, event, scroll, shallow = false, target }) {
1661
1843
  let should_block = false;
1662
1844
 
1663
- const nav = create_navigation(current, intent, url, type, scroll ?? null);
1845
+ const nav = create_navigation(current, intent, url, type, scroll ?? null, shallow, target);
1664
1846
 
1665
1847
  if (nav.navigation.type === 'popstate' && delta !== undefined) {
1666
1848
  nav.navigation.delta = delta;
@@ -1681,6 +1863,9 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1681
1863
 
1682
1864
  if (!is_navigating) {
1683
1865
  // Don't run the event during redirects
1866
+ // TODO this isn't fully right: if you do a goto(...) while another goto(...) is in progress,
1867
+ // or you click a link while a navigation is in progress, the beforNavigate calls are not triggered,
1868
+ // and maybe they should be?
1684
1869
  before_navigate_callbacks.forEach((fn) => fn(cancellable));
1685
1870
  }
1686
1871
 
@@ -1693,13 +1878,14 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1693
1878
  * url: URL;
1694
1879
  * popped?: {
1695
1880
  * state: Record<string, any>;
1696
- * scroll: { x: number, y: number };
1881
+ * scroll?: { x: number, y: number };
1697
1882
  * delta: number;
1883
+ * shallow: { params: Record<string, string> | null; route: { id: string } | null; url: URL } | null;
1698
1884
  * };
1699
- * keepfocus?: boolean;
1700
- * noscroll?: boolean;
1885
+ * reset?: boolean;
1701
1886
  * replace_state?: boolean;
1702
1887
  * state?: Record<string, any>;
1888
+ * persist_state?: boolean;
1703
1889
  * redirect_count?: number;
1704
1890
  * nav_token?: {};
1705
1891
  * accept?: () => void;
@@ -1713,10 +1899,10 @@ async function navigate({
1713
1899
  type,
1714
1900
  url,
1715
1901
  popped,
1716
- keepfocus,
1717
- noscroll,
1902
+ reset = true,
1718
1903
  replace_state,
1719
1904
  state = {},
1905
+ persist_state = false,
1720
1906
  redirect_count = 0,
1721
1907
  nav_token = {},
1722
1908
  accept = noop,
@@ -1738,6 +1924,8 @@ async function navigate({
1738
1924
  delta: popped?.delta,
1739
1925
  intent,
1740
1926
  scroll: popped?.scroll,
1927
+ shallow: !!popped?.shallow,
1928
+ target: popped?.shallow ?? undefined,
1741
1929
  // @ts-ignore
1742
1930
  event
1743
1931
  });
@@ -1821,10 +2009,10 @@ async function navigate({
1821
2009
  type,
1822
2010
  url: new URL(navigation_result.location, url),
1823
2011
  popped,
1824
- keepfocus,
1825
- noscroll,
2012
+ reset,
1826
2013
  replace_state,
1827
2014
  state,
2015
+ persist_state,
1828
2016
  redirect_count: redirect_count + 1,
1829
2017
  nav_token
1830
2018
  });
@@ -1858,7 +2046,7 @@ async function navigate({
1858
2046
 
1859
2047
  updating = true;
1860
2048
 
1861
- update_scroll_positions(previous_history_index);
2049
+ capture_scroll(previous_history_index);
1862
2050
  capture_snapshot(previous_navigation_index);
1863
2051
 
1864
2052
  // ensure the url pathname matches the page's trailing slash option
@@ -1871,15 +2059,23 @@ async function navigate({
1871
2059
  if (!popped) {
1872
2060
  // this is a new navigation, rather than a popstate
1873
2061
  const change = replace_state ? 0 : 1;
2062
+ if (type !== 'enter') {
2063
+ if (reset) current_reset_index += 1;
2064
+ }
1874
2065
 
1875
2066
  const entry = {
1876
- [HISTORY_INDEX]: (current_history_index += change),
1877
- [NAVIGATION_INDEX]: (current_navigation_index += change),
1878
- [STATES_KEY]: state
2067
+ [HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
2068
+ historyIndex: (current_history_index += change),
2069
+ navigationIndex: (current_navigation_index += change),
2070
+ state,
2071
+ persistState: persist_state,
2072
+ resetIndex: current_reset_index
2073
+ })
1879
2074
  };
1880
2075
 
1881
2076
  const fn = replace_state ? history.replaceState : history.pushState;
1882
2077
  fn.call(history, entry, '', url);
2078
+ set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
1883
2079
 
1884
2080
  if (!replace_state) {
1885
2081
  clear_onward_history(current_history_index, current_navigation_index);
@@ -1898,6 +2094,7 @@ async function navigate({
1898
2094
  }
1899
2095
 
1900
2096
  navigation_result.props.page.state = state;
2097
+ navigation_result.props.page.shallow = popped?.shallow ?? null;
1901
2098
 
1902
2099
  /**
1903
2100
  * @type {Promise<void> | undefined}
@@ -1928,7 +2125,13 @@ async function navigate({
1928
2125
  }
1929
2126
 
1930
2127
  // Type-casts are save because we know this resolved a proper SvelteKit route
1931
- const target = /** @type {import('@sveltejs/kit').NavigationTarget} */ (nav.navigation.to);
2128
+ const target = popped?.shallow
2129
+ ? {
2130
+ params: navigation_result.state.params,
2131
+ route: { id: navigation_result.state.route?.id ?? null },
2132
+ url: navigation_result.state.url
2133
+ }
2134
+ : /** @type {import('@sveltejs/kit').NavigationTarget} */ (nav.navigation.to);
1932
2135
  current = {
1933
2136
  ...navigation_result.state,
1934
2137
  nav: {
@@ -1945,13 +2148,7 @@ async function navigate({
1945
2148
 
1946
2149
  // Remove focus before updating the component tree, so that blur/focusout
1947
2150
  // handlers fire while the old component's data is still valid (#14575)
1948
- if (
1949
- !keepfocus &&
1950
- document.activeElement instanceof HTMLElement &&
1951
- document.activeElement !== document.body
1952
- ) {
1953
- document.activeElement.blur();
1954
- }
2151
+ blur_active_element(reset);
1955
2152
 
1956
2153
  const fork = load_cache_fork && (await load_cache_fork);
1957
2154
 
@@ -1960,28 +2157,22 @@ async function navigate({
1960
2157
  // `fork.commit()` applies the preloaded state synchronously before the
1961
2158
  // first `await`, so reset any previously-failed boundaries now so the
1962
2159
  // stale `+error.svelte` is torn down. See sveltejs/kit#15694.
1963
- for (const reset of resetters) {
1964
- reset?.();
2160
+ for (const reset_boundary of resetters) {
2161
+ reset_boundary();
1965
2162
  }
1966
- resetters.length = 0;
2163
+ resetters.clear();
1967
2164
  } else {
1968
- rendering_error = null; // TODO this can break with forks, rethink for SvelteKit 3 where we can assume Svelte 5
1969
- root.$set(navigation_result.props);
1970
- current_tree = navigation_result.props.tree;
2165
+ apply_navigation_result(navigation_result);
2166
+
1971
2167
  // Reset any boundaries that failed on a previous navigation now that the
1972
2168
  // new props are applied, otherwise the stale `+error.svelte` stays
1973
2169
  // mounted above the new route's content. See sveltejs/kit#15694.
1974
- for (const reset of resetters) {
1975
- reset?.();
1976
- }
1977
- resetters.length = 0;
1978
- // Check for sync rendering error
1979
- if (rendering_error) {
1980
- Object.assign(navigation_result.props.page, rendering_error);
2170
+ for (const reset_boundary of resetters) {
2171
+ reset_boundary();
1981
2172
  }
1982
- update(navigation_result.props.page);
2173
+ resetters.clear();
1983
2174
 
1984
- commit_promise = svelte.settled?.();
2175
+ commit_promise = settled();
1985
2176
  }
1986
2177
 
1987
2178
  has_navigated = true;
@@ -1991,7 +2182,7 @@ async function navigate({
1991
2182
 
1992
2183
  const { activeElement } = document;
1993
2184
 
1994
- await (commit_promise ?? svelte.tick());
2185
+ await commit_promise;
1995
2186
 
1996
2187
  if (navigation_token !== nav_token) {
1997
2188
  // a new navigation happened while we were waiting for the DOM to update, so abort
@@ -1999,44 +2190,7 @@ async function navigate({
1999
2190
  return;
2000
2191
  }
2001
2192
 
2002
- // Check for async rendering error
2003
- if (navigation_result.props.page && rendering_error) {
2004
- Object.assign(navigation_result.props.page, rendering_error);
2005
- }
2006
-
2007
- // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
2008
- /** @type {Element | null} */
2009
- let deep_linked = null;
2010
-
2011
- if (autoscroll) {
2012
- const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
2013
- if (scroll) {
2014
- scrollTo(scroll.x, scroll.y);
2015
- } else if ((deep_linked = get_hash_element(url))) {
2016
- // Here we use `scrollIntoView` on the element instead of `scrollTo`
2017
- // because it natively supports the `scroll-margin` and `scroll-behavior`
2018
- // CSS properties.
2019
- deep_linked.scrollIntoView();
2020
- } else {
2021
- scrollTo(0, 0);
2022
- }
2023
- }
2024
-
2025
- const changed_focus =
2026
- // reset focus only if any manual focus management didn't override it
2027
- document.activeElement !== activeElement &&
2028
- // also refocus when activeElement is body already because the
2029
- // focus event might not have been fired on it yet
2030
- document.activeElement !== document.body;
2031
-
2032
- if (!keepfocus && !changed_focus) {
2033
- // We don't need to manually restore the scroll position if we're navigating
2034
- // to a fragment identifier. It is automatically done for us when we set the
2035
- // sequential navigation starting point with `location.replace`
2036
- reset_focus(url, !deep_linked);
2037
- }
2038
-
2039
- autoscroll = true;
2193
+ reset_scroll_and_focus(url, reset ? popped?.scroll : scroll_state(), reset, activeElement);
2040
2194
 
2041
2195
  is_navigating = false;
2042
2196
 
@@ -2280,7 +2434,7 @@ export async function handle_error(error, event) {
2280
2434
  * @param {T} callback
2281
2435
  */
2282
2436
  function add_navigation_callback(callbacks, callback) {
2283
- svelte.onMount(() => {
2437
+ onMount(() => {
2284
2438
  callbacks.add(callback);
2285
2439
 
2286
2440
  return () => {
@@ -2352,24 +2506,49 @@ export function disableScrollHandling() {
2352
2506
  }
2353
2507
 
2354
2508
  let warned_on_invalidate_all = false;
2509
+ let warned_on_replace_state = false;
2510
+ let warned_on_push_state = false;
2511
+ let warned_on_replace_state_function = false;
2355
2512
 
2356
2513
  /**
2357
- * Allows you to navigate programmatically to a given route, with options such as keeping the current element focused.
2358
- * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
2514
+ * @param {string | URL} url
2515
+ * @param {'goto' | 'pushState' | 'replaceState'} caller
2516
+ */
2517
+ async function resolve_intent(url, caller) {
2518
+ const resolved = new URL(resolve_url(url));
2519
+
2520
+ if (resolved.origin !== origin) {
2521
+ throw new Error(
2522
+ DEV
2523
+ ? `Cannot use \`${caller}\` with an external URL. Use \`window.location = "${url}"\` instead`
2524
+ : `${caller}: invalid URL`
2525
+ );
2526
+ }
2527
+
2528
+ const intent = await get_navigation_intent(resolved, false);
2529
+
2530
+ if (!intent) {
2531
+ throw new Error(
2532
+ DEV
2533
+ ? `Cannot use \`${caller}\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
2534
+ : `${caller}: invalid URL`
2535
+ );
2536
+ }
2537
+
2538
+ return intent;
2539
+ }
2540
+
2541
+ /**
2542
+ * Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
2543
+ * (as they would be with a regular navigation) or preserved.
2359
2544
  *
2360
- * `goto` is intended for navigations to routes that belong to the app.
2361
- * If the URL does not resolve to a route within the app, the returned promise will reject.
2545
+ * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
2546
+ *
2547
+ * `goto` is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
2362
2548
  * For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
2363
2549
  *
2364
2550
  * @param {string | URL} url Where to navigate to. Note that if you've set [`config.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app.
2365
- * @param {Object} [opts] Options related to the navigation
2366
- * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
2367
- * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation
2368
- * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body
2369
- * @param {boolean} [opts.refreshAll] If `true`, all `load` functions and queries of the page will be rerun. See https://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation.
2370
- * @param {Array<string | URL | ((url: URL) => boolean)>} [opts.invalidate] Causes any load functions to re-run if they depend on one of the urls
2371
- * @param {boolean} [opts.invalidateAll] Deprecated in favour of opts.refreshAll.
2372
- * @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
2551
+ * @param {import('@sveltejs/kit').GotoOptions} [opts] Options related to the navigation
2373
2552
  * @returns {Promise<void>}
2374
2553
  */
2375
2554
  export async function goto(url, opts = {}) {
@@ -2377,23 +2556,35 @@ export async function goto(url, opts = {}) {
2377
2556
  throw new Error('Cannot call goto(...) on the server');
2378
2557
  }
2379
2558
 
2380
- url = new URL(resolve_url(url));
2559
+ if (DEV) {
2560
+ if ('replaceState' in opts && !warned_on_replace_state) {
2561
+ warned_on_replace_state = true;
2562
+ console.warn(
2563
+ `The \`goto(..., { replaceState: ${opts.replaceState} })\` option has been deprecated in favour of \`replace\``
2564
+ );
2565
+ }
2381
2566
 
2382
- if (url.origin !== origin) {
2383
- throw new Error(
2384
- DEV
2385
- ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2386
- : 'goto: invalid URL'
2387
- );
2567
+ if ('noScroll' in opts || 'keepFocus' in opts) {
2568
+ throw new Error(
2569
+ `The \`goto(..., { noScroll: true, keepFocus: true })\` options have been replaced by \`reset: false\``
2570
+ );
2571
+ }
2388
2572
  }
2389
2573
 
2390
- const intent = await get_navigation_intent(url, false);
2574
+ const replace = opts.replace ?? opts.replaceState ?? false;
2391
2575
 
2392
- if (!intent) {
2393
- throw new Error(
2394
- DEV
2395
- ? `Cannot use \`goto\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
2396
- : 'goto: invalid URL'
2576
+ const intent = await resolve_intent(url, 'goto');
2577
+
2578
+ if (opts.shallow) {
2579
+ return update_state(
2580
+ intent,
2581
+ opts.state ?? {},
2582
+ {
2583
+ replace,
2584
+ persist_state: opts.persistState ?? false,
2585
+ reset: opts.reset ?? false
2586
+ },
2587
+ 'goto'
2397
2588
  );
2398
2589
  }
2399
2590
 
@@ -2404,8 +2595,13 @@ export async function goto(url, opts = {}) {
2404
2595
  );
2405
2596
  }
2406
2597
 
2407
- opts.refreshAll = opts.refreshAll ?? opts.invalidateAll;
2408
- return _goto(url, opts, 0, {}, intent);
2598
+ return _goto(
2599
+ intent.url,
2600
+ { ...opts, replace, refreshAll: opts.refreshAll ?? opts.invalidateAll },
2601
+ 0,
2602
+ {},
2603
+ intent
2604
+ );
2409
2605
  }
2410
2606
 
2411
2607
  /**
@@ -2537,105 +2733,150 @@ export async function preloadData(href) {
2537
2733
  * Programmatically imports the code for routes that haven't yet been fetched.
2538
2734
  * Typically, you might call this to speed up subsequent navigation.
2539
2735
  *
2540
- * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about/+page.svelte`) or `/blog/*` (to match `src/routes/blog/[slug]/+page.svelte`).
2736
+ * Takes a route ID such as `/about` or `/blog/[slug]`. Unlike pathnames, route IDs
2737
+ * are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths).
2738
+ * If you have a pathname rather than a route ID, you can convert it with
2739
+ * [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`:
2740
+ *
2741
+ * ```js
2742
+ * import { match } from '$app/paths';
2743
+ * import { preloadCode } from '$app/navigation';
2744
+ *
2745
+ * const matched = await match('/blog/hello-world');
2746
+ * if (matched) await preloadCode(matched.id);
2747
+ * ```
2541
2748
  *
2542
2749
  * Unlike `preloadData`, this won't call `load` functions.
2543
2750
  * Returns a Promise that resolves when the modules have been imported.
2544
2751
  *
2545
- * @param {string} pathname
2752
+ * @param {RouteId} id
2546
2753
  * @returns {Promise<void>}
2547
2754
  */
2548
- export async function preloadCode(pathname) {
2755
+ export async function preloadCode(id) {
2549
2756
  if (!BROWSER) {
2550
2757
  throw new Error('Cannot call preloadCode(...) on the server');
2551
2758
  }
2552
2759
 
2553
- // `current.url` is null until the first navigation/hydration completes, so fall back
2554
- // to `location` to support calling `preloadCode` during initial page load (#13297)
2555
- const url = new URL(pathname, current.url ?? location.href);
2760
+ if (DEV && id[0] !== '/') {
2761
+ throw new Error(
2762
+ `argument passed to preloadCode must be a route ID (i.e. "/blog/[slug]" rather than "blog/[slug]")`
2763
+ );
2764
+ }
2556
2765
 
2557
- if (DEV) {
2558
- if (!pathname.startsWith('/')) {
2559
- throw new Error(
2560
- 'argument passed to preloadCode must be a pathname (i.e. "/about" rather than "http://example.com/about"'
2561
- );
2562
- }
2766
+ const route = __SVELTEKIT_CLIENT_ROUTING__
2767
+ ? routes.find((r) => r.id === id)
2768
+ : (route_id_cache.get(id) ?? (await load_route_by_id(id)));
2563
2769
 
2564
- if (!pathname.startsWith(base)) {
2565
- throw new Error(
2566
- `pathname passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")`
2770
+ if (route === ENDPOINT_ONLY) {
2771
+ if (DEV) {
2772
+ console.warn(
2773
+ `'${id}' has no \`+page\`, so there is no code to preload. If you meant to warm up an ` +
2774
+ `endpoint, request it with \`fetch\` instead.`
2567
2775
  );
2568
2776
  }
2569
2777
 
2570
- if (__SVELTEKIT_CLIENT_ROUTING__) {
2571
- const rerouted = await get_rerouted_url(url);
2572
- if (!rerouted || !routes.find((route) => route.exec(get_url_path(rerouted)))) {
2573
- throw new Error(`'${pathname}' did not match any routes`);
2778
+ return;
2779
+ }
2780
+
2781
+ if (!route) {
2782
+ if (DEV) {
2783
+ // warn rather than throw, since under client routing an endpoint-only route id is
2784
+ // indistinguishable from a typo — the client manifest only contains routes with a `+page`
2785
+ let message = `'${id}' did not match any route`;
2786
+
2787
+ if (__SVELTEKIT_CLIENT_ROUTING__) {
2788
+ message += ` (note that routes without a \`+page\` have no code to preload)`;
2789
+
2790
+ // the most common migration mistake is passing a pathname, which used to work
2791
+ const candidates = [id];
2792
+ if (base && id.startsWith(base)) candidates.push(id.slice(base.length) || '/');
2793
+
2794
+ if (candidates.some((path) => routes.some((r) => r.exec(path)))) {
2795
+ message += `. It does match as a pathname — use \`match(...)\` from \`$app/paths\` to convert a pathname into a route ID`;
2796
+ }
2574
2797
  }
2798
+
2799
+ console.warn(message);
2575
2800
  }
2801
+
2802
+ return;
2576
2803
  }
2577
2804
 
2578
- return _preload_code(url);
2805
+ await load_route_nodes(route);
2579
2806
  }
2580
2807
 
2581
2808
  /**
2582
- * Programmatically create a new history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2809
+ * Programmatically create a new history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2583
2810
  *
2811
+ * @deprecated Use `goto(url, { state, shallow: true })` instead.
2584
2812
  * @param {string | URL} url
2585
2813
  * @param {App.PageState} state
2586
- * @returns {void}
2814
+ * @returns {Promise<void>}
2587
2815
  */
2588
- export function pushState(url, state) {
2816
+ export async function pushState(url, state) {
2589
2817
  if (!BROWSER) {
2590
2818
  throw new Error('Cannot call pushState(...) on the server');
2591
2819
  }
2592
2820
 
2593
- if (DEV) {
2594
- if (!started) {
2595
- throw new Error('Cannot call pushState(...) before router is initialized');
2596
- }
2597
-
2598
- try {
2599
- // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2600
- devalue.stringify(state);
2601
- } catch (error) {
2602
- // @ts-expect-error
2603
- throw new Error(`Could not serialize state${error.path}`, { cause: error });
2604
- }
2821
+ if (DEV && !warned_on_push_state) {
2822
+ warned_on_push_state = true;
2823
+ console.warn(
2824
+ '`pushState(...)` is deprecated. Use `goto(url, { state, shallow: true })` instead.'
2825
+ );
2605
2826
  }
2606
2827
 
2607
- update_scroll_positions(current_history_index);
2828
+ const intent = await resolve_intent(url, 'pushState');
2608
2829
 
2609
- const opts = {
2610
- [HISTORY_INDEX]: (current_history_index += 1),
2611
- [NAVIGATION_INDEX]: current_navigation_index,
2612
- [PAGE_URL_KEY]: page.url.href,
2613
- [STATES_KEY]: state
2614
- };
2615
-
2616
- history.pushState(opts, '', resolve_url(url));
2617
- has_navigated = true;
2618
-
2619
- page.state = state;
2620
-
2621
- clear_onward_history(current_history_index, current_navigation_index);
2830
+ await update_state(
2831
+ intent,
2832
+ state,
2833
+ { replace: false, persist_state: false, reset: false },
2834
+ 'pushState'
2835
+ );
2622
2836
  }
2623
2837
 
2624
2838
  /**
2625
- * Programmatically replace the current history entry with the given `page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2839
+ * Programmatically replace the current history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2626
2840
  *
2841
+ * @deprecated Use `goto(url, { state, shallow: true, replace: true })` instead.
2627
2842
  * @param {string | URL} url
2628
2843
  * @param {App.PageState} state
2629
- * @returns {void}
2844
+ * @returns {Promise<void>}
2630
2845
  */
2631
- export function replaceState(url, state) {
2846
+ export async function replaceState(url, state) {
2632
2847
  if (!BROWSER) {
2633
2848
  throw new Error('Cannot call replaceState(...) on the server');
2634
2849
  }
2635
2850
 
2851
+ if (DEV && !warned_on_replace_state_function) {
2852
+ warned_on_replace_state_function = true;
2853
+ console.warn(
2854
+ '`replaceState(...)` is deprecated. Use `goto(url, { state, shallow: true, replace: true })` instead.'
2855
+ );
2856
+ }
2857
+
2858
+ const intent = await resolve_intent(url, 'replaceState');
2859
+
2860
+ await update_state(
2861
+ intent,
2862
+ state,
2863
+ { replace: true, persist_state: false, reset: false },
2864
+ 'replaceState'
2865
+ );
2866
+ }
2867
+
2868
+ /**
2869
+ * @param {NavigationIntent} intent
2870
+ * @param {App.PageState} state
2871
+ * @param {{ replace: boolean; persist_state: boolean; reset: boolean; }} options
2872
+ * @param {'goto' | 'pushState' | 'replaceState'} caller
2873
+ */
2874
+ async function update_state(intent, state, { replace, persist_state, reset }, caller) {
2875
+ const url = intent.url;
2876
+
2636
2877
  if (DEV) {
2637
2878
  if (!started) {
2638
- throw new Error('Cannot call replaceState(...) before router is initialized');
2879
+ throw new Error(`Cannot call ${caller}(...) before router is initialized`);
2639
2880
  }
2640
2881
 
2641
2882
  try {
@@ -2647,16 +2888,100 @@ export function replaceState(url, state) {
2647
2888
  }
2648
2889
  }
2649
2890
 
2650
- const opts = {
2651
- [HISTORY_INDEX]: current_history_index,
2652
- [NAVIGATION_INDEX]: current_navigation_index,
2653
- [PAGE_URL_KEY]: page.url.href,
2654
- [STATES_KEY]: state
2891
+ const nav =
2892
+ // For backwards compatibility we don't trigger navigation hooks etc for push/replaceState
2893
+ caller === 'goto' ? _before_navigate({ url, type: 'goto', intent, shallow: true }) : undefined;
2894
+
2895
+ if (!nav && caller === 'goto') return;
2896
+
2897
+ const nav_token = {};
2898
+
2899
+ if (nav) {
2900
+ navigation_token = invalidation_token = nav_token;
2901
+ is_navigating = true;
2902
+ navigating.current = nav.navigation;
2903
+ updating = true;
2904
+ }
2905
+
2906
+ if (!replace) capture_scroll(current_history_index);
2907
+ if (reset) current_reset_index += 1;
2908
+
2909
+ const entry = {
2910
+ [HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
2911
+ historyIndex: (current_history_index += replace ? 0 : 1),
2912
+ navigationIndex: current_navigation_index,
2913
+ pageUrl: page.url.href,
2914
+ state,
2915
+ persistState: persist_state,
2916
+ resetIndex: current_reset_index
2917
+ })
2655
2918
  };
2656
2919
 
2657
- history.replaceState(opts, '', resolve_url(url));
2920
+ const fn = replace ? history.replaceState : history.pushState;
2921
+ fn.call(history, entry, '', url);
2922
+ set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
2923
+
2924
+ if (!replace) {
2925
+ has_navigated = true;
2926
+ clear_onward_history(current_history_index, current_navigation_index);
2927
+ }
2928
+
2929
+ if (nav) {
2930
+ const after_navigate = (
2931
+ await Promise.all(
2932
+ // eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
2933
+ Array.from(on_navigate_callbacks, (fn) =>
2934
+ fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
2935
+ )
2936
+ )
2937
+ ).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
2938
+
2939
+ if (after_navigate.length > 0) {
2940
+ function cleanup() {
2941
+ after_navigate.forEach((fn) => after_navigate_callbacks.delete(fn));
2942
+ }
2943
+
2944
+ after_navigate.push(cleanup);
2945
+ after_navigate.forEach((fn) => after_navigate_callbacks.add(fn));
2946
+ }
2947
+ }
2948
+
2949
+ blur_active_element(reset);
2658
2950
 
2659
2951
  page.state = state;
2952
+ page.shallow = {
2953
+ params: intent?.params ?? null,
2954
+ route: intent ? { id: intent.route.id } : null,
2955
+ url
2956
+ };
2957
+
2958
+ if (!nav) return;
2959
+
2960
+ const { activeElement } = document;
2961
+
2962
+ await settled();
2963
+
2964
+ if (navigation_token !== nav_token) {
2965
+ // a new navigation happened while we were waiting for the DOM to update, so abort
2966
+ nav.reject(new Error('navigation aborted'));
2967
+ return;
2968
+ }
2969
+
2970
+ reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
2971
+
2972
+ is_navigating = false;
2973
+ nav.fulfil(undefined);
2974
+
2975
+ if (nav.navigation.to) {
2976
+ nav.navigation.to.scroll = scroll_state();
2977
+ }
2978
+
2979
+ after_navigate_callbacks.forEach((fn) =>
2980
+ fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
2981
+ );
2982
+
2983
+ navigating.current = null;
2984
+ updating = false;
2660
2985
  }
2661
2986
 
2662
2987
  /**
@@ -2675,21 +3000,19 @@ export async function applyAction(result) {
2675
3000
  if (result.type === 'error') {
2676
3001
  await set_nearest_error_page(result.error);
2677
3002
  } else if (result.type === 'redirect') {
2678
- await _goto(result.location, { refreshAll: true }, 0);
3003
+ await _goto(result.location, { refreshAll: true });
2679
3004
  } else {
2680
3005
  page.form = result.data;
2681
3006
  page.status = result.status;
2682
3007
 
2683
3008
  /** @type {Record<string, any>} */
2684
- root.$set({
2685
- // this brings Svelte's view of the world in line with SvelteKit's
2686
- // after use:enhance reset the form....
2687
- form: null
2688
- });
3009
+ // this brings Svelte's view of the world in line with SvelteKit's
3010
+ // after use:enhance reset the form....
3011
+ props.form = null;
2689
3012
 
2690
3013
  // ...so that setting the `form` prop takes effect and isn't ignored
2691
- await svelte.tick();
2692
- root.$set({ form: result.data });
3014
+ await tick();
3015
+ props.form = result.data;
2693
3016
 
2694
3017
  if (result.type === 'success') {
2695
3018
  reset_focus(/** @type {URL} */ (page.url));
@@ -2719,11 +3042,9 @@ export async function set_nearest_error_page(error) {
2719
3042
 
2720
3043
  current = { ...navigation_result.state, nav: current.nav };
2721
3044
 
2722
- root.$set(navigation_result.props);
2723
- current_tree = navigation_result.props.tree;
2724
- update(navigation_result.props.page);
3045
+ apply_navigation_result(navigation_result);
2725
3046
 
2726
- void svelte.tick().then(() => reset_focus(current.url));
3047
+ void tick().then(() => reset_focus(current.url));
2727
3048
  }
2728
3049
  }
2729
3050
 
@@ -2866,17 +3187,21 @@ function _start_router() {
2866
3187
  return;
2867
3188
  }
2868
3189
  // set this flag to distinguish between navigations triggered by
2869
- // clicking a hash link and those triggered by popstate
2870
- hash_navigating = true;
3190
+ // clicking a hash link and those triggered by popstate. We gotta retrieve
3191
+ // history metadata here because the hashchange event will occur after history.state was updated
3192
+ hash_navigating = {
3193
+ .../** @type {HistoryMetadata} */ (get_history_metadata()),
3194
+ resetIndex: current_reset_index + (options.reset ? 1 : 0)
3195
+ };
2871
3196
 
2872
- update_scroll_positions(current_history_index);
3197
+ capture_scroll(current_history_index);
2873
3198
 
2874
3199
  update_url(url);
2875
3200
 
2876
3201
  if (!options.replace_state) return;
2877
3202
 
2878
3203
  // hashchange event shouldn't occur if the router is replacing state.
2879
- hash_navigating = false;
3204
+ hash_navigating = null;
2880
3205
  }
2881
3206
 
2882
3207
  event.preventDefault();
@@ -2891,12 +3216,13 @@ function _start_router() {
2891
3216
  setTimeout(fulfil, 100); // fallback for edge case where rAF doesn't fire because e.g. tab was backgrounded
2892
3217
  });
2893
3218
 
2894
- await navigate({
3219
+ const changed = url.href !== location.href;
3220
+
3221
+ await _goto(url, {
2895
3222
  type: 'link',
2896
- url,
2897
- keepfocus: options.keepfocus,
2898
- noscroll: options.noscroll,
2899
- replace_state: options.replace_state ?? url.href === location.href,
3223
+ reset: options.reset,
3224
+ replace: options.replace_state ?? !changed,
3225
+ refreshAll: !changed,
2900
3226
  event
2901
3227
  });
2902
3228
  });
@@ -2941,8 +3267,7 @@ function _start_router() {
2941
3267
  void navigate({
2942
3268
  type: 'form',
2943
3269
  url,
2944
- keepfocus: options.keepfocus,
2945
- noscroll: options.noscroll,
3270
+ reset: options.reset,
2946
3271
  replace_state: options.replace_state ?? url.href === location.href,
2947
3272
  event
2948
3273
  });
@@ -2951,53 +3276,82 @@ function _start_router() {
2951
3276
  addEventListener('popstate', async (event) => {
2952
3277
  if (resetting_focus) return;
2953
3278
 
2954
- if (event.state?.[HISTORY_INDEX]) {
2955
- const history_index = event.state[HISTORY_INDEX];
3279
+ const history_metadata = get_history_metadata(event.state);
3280
+
3281
+ if (history_metadata?.historyIndex) {
3282
+ const history_index = history_metadata.historyIndex;
3283
+ const source_info = history_info[current_history_index];
2956
3284
  navigation_token = invalidation_token = {};
2957
3285
 
2958
3286
  // if a popstate-driven navigation is cancelled, we need to counteract it
2959
3287
  // with history.go, which means we end up back here, hence this check
2960
3288
  if (history_index === current_history_index) return;
2961
3289
 
2962
- const scroll = scroll_positions[history_index];
2963
- const state = event.state[STATES_KEY] ?? {};
2964
- const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
2965
- const navigation_index = event.state[NAVIGATION_INDEX];
2966
- const is_hash_change = current.url ? strip_hash(location) === strip_hash(current.url) : false;
3290
+ const delta = history_index - current_history_index;
3291
+ const reset_index = history_metadata.resetIndex;
3292
+ const reset = reset_index !== (source_info?.resetIndex ?? current_reset_index);
3293
+ const scroll = history_info[history_index]?.scroll;
3294
+ const state = history_metadata.state;
3295
+ const url = new URL(history_metadata.pageUrl ?? location.href);
3296
+ const navigation_index = history_metadata.navigationIndex;
3297
+ const is_hash_change =
3298
+ current.url && (location.href + current.url.href).includes('#') // check if even has a hash
3299
+ ? strip_hash(location) === strip_hash(current.url)
3300
+ : false;
2967
3301
  const shallow =
2968
- navigation_index === current_navigation_index && (has_navigated || is_hash_change);
3302
+ navigation_index === current_navigation_index &&
3303
+ ((has_navigated &&
3304
+ (history_metadata.pageUrl === undefined || history_metadata.pageUrl === location.href)) ||
3305
+ is_hash_change);
3306
+ const shallow_url = history_metadata.pageUrl ? new URL(location.href) : null;
3307
+ const shallow_intent = shallow_url
3308
+ ? await get_navigation_intent(shallow_url, false)
3309
+ : undefined;
3310
+ const shallow_target = shallow_url
3311
+ ? {
3312
+ params: shallow_intent?.params ?? null,
3313
+ route: shallow_intent ? { id: shallow_intent.route.id } : null,
3314
+ url: shallow_url
3315
+ }
3316
+ : null;
2969
3317
 
2970
3318
  if (shallow) {
2971
3319
  // We don't need to navigate, we just need to update scroll and/or state.
2972
3320
  // This happens with hash links and `pushState`/`replaceState`. The
2973
3321
  // exception is if we haven't navigated yet, since we could have
2974
3322
  // got here after a modal navigation then a reload
3323
+
3324
+ blur_active_element(reset);
3325
+
2975
3326
  if (state !== page.state) {
2976
3327
  page.state = state;
2977
3328
  }
2978
3329
 
2979
- update_url(url);
3330
+ page.shallow = shallow_target;
2980
3331
 
2981
- scroll_positions[current_history_index] = scroll_state();
2982
- if (scroll) scrollTo(scroll.x, scroll.y);
3332
+ update_url(url);
2983
3333
 
3334
+ capture_scroll(current_history_index);
2984
3335
  current_history_index = history_index;
3336
+ current_reset_index = reset_index;
3337
+ if (reset && scroll) scrollTo(scroll.x, scroll.y);
2985
3338
  return;
2986
3339
  }
2987
3340
 
2988
- const delta = history_index - current_history_index;
2989
-
2990
3341
  await navigate({
2991
3342
  type: 'popstate',
2992
3343
  url,
3344
+ reset,
2993
3345
  popped: {
2994
3346
  state,
2995
3347
  scroll,
2996
- delta
3348
+ delta,
3349
+ shallow: shallow_target
2997
3350
  },
2998
3351
  accept: () => {
2999
3352
  current_history_index = history_index;
3000
3353
  current_navigation_index = navigation_index;
3354
+ current_reset_index = reset_index;
3001
3355
  },
3002
3356
  block: () => {
3003
3357
  history.go(-delta);
@@ -3026,16 +3380,22 @@ function _start_router() {
3026
3380
  // if the hashchange happened as a result of clicking on a link,
3027
3381
  // we need to update history, otherwise we have to leave it alone
3028
3382
  if (hash_navigating) {
3029
- hash_navigating = false;
3383
+ const history_metadata = hash_navigating;
3384
+ hash_navigating = null;
3385
+ current_reset_index = history_metadata.resetIndex;
3030
3386
  history.replaceState(
3031
3387
  {
3032
3388
  ...history.state,
3033
- [HISTORY_INDEX]: ++current_history_index,
3034
- [NAVIGATION_INDEX]: current_navigation_index
3389
+ [HISTORY_METADATA_KEY]: {
3390
+ ...history_metadata,
3391
+ historyIndex: ++current_history_index,
3392
+ navigationIndex: current_navigation_index
3393
+ }
3035
3394
  },
3036
3395
  '',
3037
3396
  location.href
3038
3397
  );
3398
+ set_history_options(current_history_index, history_metadata);
3039
3399
  }
3040
3400
  });
3041
3401
 
@@ -3093,7 +3453,7 @@ async function _hydrate(
3093
3453
  } else {
3094
3454
  // undefined in case of 404
3095
3455
  if (server_route) {
3096
- parsed_route = route = parse_server_route(server_route, app.nodes);
3456
+ parsed_route = route = parse_and_cache_server_route(server_route);
3097
3457
  } else {
3098
3458
  route = { id: null };
3099
3459
  params = {};
@@ -3102,7 +3462,7 @@ async function _hydrate(
3102
3462
 
3103
3463
  /** @type {import('./types.js').NavigationFinished | undefined} */
3104
3464
  let result;
3105
- let hydrate = true;
3465
+ let should_hydrate = true;
3106
3466
 
3107
3467
  try {
3108
3468
  const branch_promises = node_ids.map(async (n, i) => {
@@ -3168,7 +3528,7 @@ async function _hydrate(
3168
3528
  });
3169
3529
 
3170
3530
  target.textContent = '';
3171
- hydrate = false;
3531
+ should_hydrate = false;
3172
3532
  }
3173
3533
 
3174
3534
  // Exit early when we encounter a redirect while loading the root error page.
@@ -3176,10 +3536,11 @@ async function _hydrate(
3176
3536
  if (!result) return;
3177
3537
 
3178
3538
  if (result.props.page) {
3179
- result.props.page.state = {};
3539
+ const history_metadata = get_history_metadata();
3540
+ result.props.page.state = history_metadata?.persistState ? history_metadata.state : {};
3180
3541
  }
3181
3542
 
3182
- await initialize(result, target, hydrate);
3543
+ await initialize(result, target, should_hydrate);
3183
3544
  }
3184
3545
 
3185
3546
  /**
@@ -3206,20 +3567,19 @@ async function load_data(url, invalid) {
3206
3567
  notify_version(res.headers.get('x-sveltekit-version'));
3207
3568
 
3208
3569
  if (!res.ok) {
3209
- // error message is a JSON-stringified string which devalue can't handle at the top level
3210
3570
  // turn it into a HttpError to not call handleError on the client again (was already handled on the server)
3211
3571
  // if `__data.json` doesn't exist or the server has an internal error,
3212
3572
  // avoid parsing the HTML error page as a JSON
3213
- /** @type {string | undefined} */
3214
- let message;
3573
+ /** @type {App.Error} */
3574
+ let error = { status: res.status, message: 'Internal Error' };
3575
+
3215
3576
  if (res.headers.get('content-type')?.includes('application/json')) {
3216
- message = await res.json();
3577
+ error = { status: res.status, ...(await res.json()) };
3217
3578
  } else if (res.status === 404) {
3218
- message = 'Not Found';
3219
- } else if (res.status === 500) {
3220
- message = 'Internal Error';
3579
+ error.message = 'Not Found';
3221
3580
  }
3222
- throw new HttpError(res.status, message);
3581
+
3582
+ throw new HttpError(error);
3223
3583
  }
3224
3584
 
3225
3585
  return new Promise((resolve, reject) => {
@@ -3412,8 +3772,18 @@ function reset_focus(url, scroll = true) {
3412
3772
  * @param {URL | null} url
3413
3773
  * @param {T} type
3414
3774
  * @param {{ x: number, y: number } | null} [target_scroll] The scroll position for the target (for popstate navigations)
3775
+ * @param {boolean} [shallow]
3776
+ * @param {{ params: Record<string, string> | null; route: { id: string } | null; url: URL }} [target]
3415
3777
  */
3416
- function create_navigation(current, intent, url, type, target_scroll = null) {
3778
+ function create_navigation(
3779
+ current,
3780
+ intent,
3781
+ url,
3782
+ type,
3783
+ target_scroll = null,
3784
+ shallow = false,
3785
+ target
3786
+ ) {
3417
3787
  /** @type {(value: any) => void} */
3418
3788
  let fulfil;
3419
3789
 
@@ -3437,13 +3807,14 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
3437
3807
  scroll: scroll_state()
3438
3808
  },
3439
3809
  to: url && {
3440
- params: intent?.params ?? null,
3441
- route: { id: intent?.route?.id ?? null },
3442
- url,
3810
+ params: target ? target.params : (intent?.params ?? null),
3811
+ route: { id: target ? (target.route?.id ?? null) : (intent?.route?.id ?? null) },
3812
+ url: target?.url ?? url,
3443
3813
  scroll: target_scroll
3444
3814
  },
3445
- willUnload: !intent,
3815
+ willUnload: !shallow && !intent,
3446
3816
  type,
3817
+ shallow,
3447
3818
  complete
3448
3819
  });
3449
3820
 
@@ -3516,3 +3887,21 @@ if (DEV) {
3516
3887
  });
3517
3888
  }
3518
3889
  }
3890
+
3891
+ /**
3892
+ * @param {NavigationFinished} result
3893
+ */
3894
+ function apply_navigation_result(result) {
3895
+ Object.assign(page, result.props.page);
3896
+
3897
+ props.tree.data = result.props.tree.data;
3898
+ props.tree.child = result.props.tree.child;
3899
+
3900
+ if ('form' in result.props) {
3901
+ props.form = result.props.form;
3902
+ }
3903
+
3904
+ if ('error' in result.props) {
3905
+ props.error = result.props.error;
3906
+ }
3907
+ }