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

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 (46) hide show
  1. package/package.json +10 -10
  2. package/src/core/postbuild/prerender.js +3 -3
  3. package/src/core/sync/write_app_types.js +1 -2
  4. package/src/core/sync/write_tsconfig/index.js +13 -1
  5. package/src/exports/index.js +23 -12
  6. package/src/exports/internal/shared.js +4 -12
  7. package/src/exports/public.d.ts +59 -11
  8. package/src/exports/vite/build/remote.js +10 -12
  9. package/src/exports/vite/dev/index.js +32 -19
  10. package/src/exports/vite/index.js +118 -123
  11. package/src/runtime/app/server/remote/form.js +9 -1
  12. package/src/runtime/app/server/remote/prerender.js +13 -9
  13. package/src/runtime/app/server/remote/requested.js +4 -4
  14. package/src/runtime/app/state/client.js +3 -0
  15. package/src/runtime/app/state/index.js +2 -2
  16. package/src/runtime/app/state/server.js +3 -0
  17. package/src/runtime/client/client.js +584 -305
  18. package/src/runtime/client/constants.js +2 -6
  19. package/src/runtime/client/fetcher.js +27 -17
  20. package/src/runtime/client/remote-functions/form.svelte.js +39 -7
  21. package/src/runtime/client/remote-functions/prerender.svelte.js +1 -1
  22. package/src/runtime/client/remote-functions/query/instance.svelte.js +2 -2
  23. package/src/runtime/client/remote-functions/query-batch.svelte.js +1 -1
  24. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +2 -2
  25. package/src/runtime/client/remote-functions/query-live/iterator.js +10 -8
  26. package/src/runtime/client/remote-functions/shared.svelte.js +11 -10
  27. package/src/runtime/client/state.svelte.js +10 -22
  28. package/src/runtime/client/types.d.ts +1 -2
  29. package/src/runtime/client/utils.js +21 -14
  30. package/src/runtime/components/root.svelte +4 -14
  31. package/src/runtime/props.svelte.js +71 -0
  32. package/src/runtime/server/fetch.js +4 -6
  33. package/src/runtime/server/page/csp.js +86 -103
  34. package/src/runtime/server/page/index.js +1 -2
  35. package/src/runtime/server/page/load_data.js +15 -5
  36. package/src/runtime/server/page/render.js +27 -24
  37. package/src/runtime/server/page/respond_with_error.js +1 -3
  38. package/src/runtime/server/respond.js +0 -2
  39. package/src/types/ambient-private.d.ts +1 -1
  40. package/src/types/ambient.d.ts +1 -1
  41. package/src/types/internal.d.ts +2 -7
  42. package/src/types/private.d.ts +3 -12
  43. package/src/version.js +1 -1
  44. package/types/index.d.ts +92 -57
  45. package/types/index.d.ts.map +2 -1
  46. 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';
@@ -25,13 +24,10 @@ import {
25
24
  import { base, 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,29 @@ 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
44
  import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
49
45
  import { noop_span } from '../telemetry/noop.js';
50
46
  import { read_ndjson } from './ndjson.js';
51
- import RootModern from '../components/root.svelte';
52
- import { asClassComponent } from 'svelte/legacy';
47
+ import Root from '../components/root.svelte';
48
+ import { Props, RenderNode } from '../props.svelte.js';
53
49
 
54
- const Root = asClassComponent(RootModern);
50
+ /**
51
+ * @typedef {{
52
+ * historyIndex: number;
53
+ * navigationIndex: number;
54
+ * pageUrl?: string;
55
+ * state: Record<string, any>;
56
+ * persistState: boolean;
57
+ * resetIndex: number;
58
+ * }} HistoryMetadata
59
+ */
55
60
 
56
61
  export { load_css };
57
62
  const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
58
63
 
59
64
  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
65
 
69
66
  /**
70
67
  * `reset` functions for `<svelte:boundary>`s in the generated root that have
@@ -73,19 +70,18 @@ let rendering_error = null;
73
70
  * navigation away from a render error would leave the stale `+error.svelte`
74
71
  * mounted. The boundary's `onerror` populates this array; `navigate` drains it
75
72
  * after applying the new props. See sveltejs/kit#15694.
76
- * @type {Array<(() => void) | undefined>}
73
+ * @type {Set<() => void>}
77
74
  */
78
- const resetters = [];
75
+ const resetters = new Set();
79
76
 
80
- // We track the scroll position associated with each history entry in sessionStorage,
77
+ // We track information associated with each history entry in sessionStorage,
81
78
  // 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
79
+ // popstate it's too late to access the options or update the focus position associated with the
83
80
  // state we're navigating from
84
81
  /**
85
- * history index -> { x, y }
86
- * @type {Record<number, { x: number; y: number }>}
82
+ * @type {Record<number, { scroll?: { x: number; y: number }; resetIndex?: number }>}
87
83
  */
88
- const scroll_positions = storage.get(SCROLL_KEY) ?? {};
84
+ const history_info = storage.get(HISTORY_INFO_KEY) ?? {};
89
85
 
90
86
  /**
91
87
  * navigation index -> any
@@ -93,14 +89,14 @@ const scroll_positions = storage.get(SCROLL_KEY) ?? {};
93
89
  */
94
90
  const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
95
91
 
96
- /**
97
- * @deprecated this is a temporary measure to avoid a regression, replace with nested `RenderNode` classes
98
- */
99
- let current_tree = /** @type {RenderNode} */ ({});
92
+ /** @type {Props} */
93
+ let props;
100
94
 
101
95
  if (DEV && BROWSER) {
102
96
  let warned = false;
103
97
 
98
+ 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
99
+
104
100
  const warn = () => {
105
101
  if (warned) return;
106
102
 
@@ -113,14 +109,15 @@ if (DEV && BROWSER) {
113
109
  // skip over `warn` and the place where `warn` was called
114
110
  const frame = stack[2];
115
111
 
116
- // ignore calls that happen inside dependencies, including SvelteKit.
112
+ // Ignore calls that happen inside dependencies, including SvelteKit.
113
+ // 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
114
  // `frame` can be falsy if we came from an anonymous function
118
- if (frame?.includes('node_modules')) return;
115
+ if (frame?.includes('node_modules') || frame?.includes(current_module_url)) return;
119
116
 
120
117
  warned = true;
121
118
 
122
119
  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."
120
+ "Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use `goto(...)` from `$app/navigation` instead."
124
121
  );
125
122
  };
126
123
 
@@ -138,8 +135,60 @@ if (DEV && BROWSER) {
138
135
  }
139
136
 
140
137
  /** @param {number} index */
141
- function update_scroll_positions(index) {
142
- scroll_positions[index] = scroll_state();
138
+ function capture_scroll(index) {
139
+ history_info[index].scroll = scroll_state();
140
+ }
141
+
142
+ /**
143
+ * @param {number} index
144
+ * @param {Pick<HistoryMetadata, 'resetIndex'>} options
145
+ */
146
+ function set_history_options(index, options) {
147
+ history_info[index] = {
148
+ ...history_info[index],
149
+ resetIndex: options.resetIndex
150
+ };
151
+ }
152
+
153
+ /** @param {boolean} reset */
154
+ function blur_active_element(reset) {
155
+ if (
156
+ reset &&
157
+ document.activeElement instanceof HTMLElement &&
158
+ document.activeElement !== document.body
159
+ ) {
160
+ document.activeElement.blur();
161
+ }
162
+ }
163
+
164
+ /**
165
+ * @param {URL} url
166
+ * @param {{ x: number; y: number } | null | undefined} scroll
167
+ * @param {boolean} reset
168
+ * @param {Element | null} active_element
169
+ */
170
+ function reset_scroll_and_focus(url, scroll, reset, active_element) {
171
+ /** @type {Element | null} */
172
+ let deep_linked = null;
173
+
174
+ if (autoscroll) {
175
+ if (scroll) {
176
+ scrollTo(scroll.x, scroll.y);
177
+ } else if ((deep_linked = get_hash_element(url))) {
178
+ deep_linked.scrollIntoView();
179
+ } else {
180
+ scrollTo(0, 0);
181
+ }
182
+ }
183
+
184
+ const changed_focus =
185
+ document.activeElement !== active_element && document.activeElement !== document.body;
186
+
187
+ if (reset && !changed_focus) {
188
+ reset_focus(url, !deep_linked);
189
+ }
190
+
191
+ autoscroll = true;
143
192
  }
144
193
 
145
194
  /**
@@ -150,8 +199,8 @@ function clear_onward_history(current_history_index, current_navigation_index) {
150
199
  // if we navigated back, then pushed a new state, we can
151
200
  // release memory by pruning the scroll/snapshot lookup
152
201
  let i = current_history_index + 1;
153
- while (scroll_positions[i]) {
154
- delete scroll_positions[i];
202
+ while (history_info[i]) {
203
+ delete history_info[i];
155
204
  i += 1;
156
205
  }
157
206
 
@@ -225,14 +274,6 @@ export const prerender_responses = {};
225
274
  /** @type {Array<((url: URL) => boolean)>} */
226
275
  const invalidated = [];
227
276
 
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
277
  /** @type {{id: string, token: {}, promise: Promise<import('./types.js').NavigationResult>, fork: Promise<import('svelte').Fork | null> | null} | null} */
237
278
  let load_cache = null;
238
279
 
@@ -282,21 +323,30 @@ let started = false;
282
323
  let autoscroll = true;
283
324
  let updating = false;
284
325
  let is_navigating = false;
285
- let hash_navigating = false;
326
+ /** @type {HistoryMetadata | null} */
327
+ let hash_navigating = null;
286
328
  /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
287
329
  let has_navigated = false;
288
330
 
289
331
  let force_invalidation = false;
290
332
 
291
- /** @type {import('svelte').SvelteComponent} */
292
- let root;
293
-
294
333
  /** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
295
334
  let current_history_index;
296
335
 
297
336
  /** @type {number} */
298
337
  let current_navigation_index;
299
338
 
339
+ /** @type {number} */
340
+ let current_reset_index;
341
+
342
+ /**
343
+ * @param {any} [state]
344
+ * @returns {HistoryMetadata | undefined}
345
+ */
346
+ function get_history_metadata(state = history.state) {
347
+ return state?.[HISTORY_METADATA_KEY];
348
+ }
349
+
300
350
  /** @type {{}} Token for the latest navigation. Updated on new navigations */
301
351
  let navigation_token;
302
352
 
@@ -319,7 +369,7 @@ let invalidation_token;
319
369
  const preload_tokens = new Set();
320
370
 
321
371
  /** @type {Promise<void> | null} */
322
- export let pending_invalidate;
372
+ let pending_invalidate;
323
373
 
324
374
  /**
325
375
  * @type {Map<string, Map<string, CacheEntry<Query<any>>>>}
@@ -350,12 +400,33 @@ set_match_implementation(async (url) => {
350
400
  return null;
351
401
  });
352
402
 
403
+ let embedded_start = Promise.resolve();
404
+
405
+ /**
406
+ * TODO this indirection is a grotesque workaround for the fact that multiple apps
407
+ * can operate on the same shared mutable state. This is fundamentally unsound,
408
+ * and the `start`/`_start` distinction does not fix it, but it does get the
409
+ * tests passing. We need to rethink this whole thing but not right now
410
+ * @param {import('./types.js').SvelteKitApp} _app
411
+ * @param {HTMLElement} _target
412
+ * @param {Parameters<typeof _hydrate>[1]} [data]
413
+ */
414
+ export function start(_app, _target, data) {
415
+ if (__SVELTEKIT_EMBEDDED__) {
416
+ const start_promise = embedded_start.then(() => _start(_app, _target, data));
417
+ embedded_start = start_promise.catch(noop);
418
+ return start_promise;
419
+ }
420
+
421
+ return _start(_app, _target, data);
422
+ }
423
+
353
424
  /**
354
425
  * @param {import('./types.js').SvelteKitApp} _app
355
426
  * @param {HTMLElement} _target
356
- * @param {Parameters<typeof _hydrate>[1]} [hydrate]
427
+ * @param {Parameters<typeof _hydrate>[1]} [data]
357
428
  */
358
- export async function start(_app, _target, hydrate) {
429
+ async function _start(_app, _target, data) {
359
430
  if (DEV && _target === document.body) {
360
431
  console.warn(
361
432
  '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 +464,56 @@ export async function start(_app, _target, hydrate) {
393
464
  // connectivity errors after initialisation don't nuke the app
394
465
  default_layout_loader = _app.nodes[0];
395
466
  default_error_loader = _app.nodes[1];
396
- void default_layout_loader();
397
- void default_error_loader();
398
467
 
399
- current_history_index = history.state?.[HISTORY_INDEX];
400
- current_navigation_index = history.state?.[NAVIGATION_INDEX];
468
+ const [root_layout, root_error] = await Promise.all([
469
+ default_layout_loader(),
470
+ default_error_loader()
471
+ ]);
472
+
473
+ const tree = new RenderNode(root_layout.component, root_error.component);
474
+
475
+ props = new Props({
476
+ page,
477
+ tree,
478
+ form: undefined,
479
+ error: undefined,
480
+ onerror: (_, reset) => resetters.add(reset)
481
+ });
482
+
483
+ const history_metadata = get_history_metadata();
484
+ current_history_index = history_metadata?.historyIndex ?? 0;
485
+ current_navigation_index = history_metadata?.navigationIndex ?? 0;
486
+ current_reset_index = history_metadata?.resetIndex ?? 0;
401
487
 
402
488
  if (!current_history_index) {
403
489
  // we use Date.now() as an offset so that cross-document navigations
404
490
  // within the app don't result in data loss
405
- current_history_index = current_navigation_index = Date.now();
491
+ current_history_index = current_navigation_index = current_reset_index = Date.now();
406
492
 
407
493
  // create initial history entry, so we can return here
408
494
  history.replaceState(
409
495
  {
410
496
  ...history.state,
411
- [HISTORY_INDEX]: current_history_index,
412
- [NAVIGATION_INDEX]: current_navigation_index
497
+ [HISTORY_METADATA_KEY]: {
498
+ historyIndex: current_history_index,
499
+ navigationIndex: current_navigation_index,
500
+ state: {},
501
+ persistState: false,
502
+ resetIndex: current_history_index
503
+ }
413
504
  },
414
505
  ''
415
506
  );
416
507
  }
417
508
 
509
+ set_history_options(
510
+ current_history_index,
511
+ /** @type {HistoryMetadata} */ (get_history_metadata())
512
+ );
513
+
418
514
  // if we reload the page, or Cmd-Shift-T back to it,
419
515
  // recover scroll position
420
- const scroll = scroll_positions[current_history_index];
516
+ const scroll = history_info[current_history_index]?.scroll;
421
517
  function restore_scroll() {
422
518
  if (scroll) {
423
519
  history.scrollRestoration = 'manual';
@@ -425,15 +521,17 @@ export async function start(_app, _target, hydrate) {
425
521
  }
426
522
  }
427
523
 
428
- if (hydrate) {
524
+ if (data) {
429
525
  restore_scroll();
430
526
 
431
- await _hydrate(target, hydrate);
527
+ await _hydrate(target, data);
432
528
  } else {
433
529
  await navigate({
434
530
  type: 'enter',
435
531
  url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href),
436
- replace_state: true
532
+ replace_state: true,
533
+ state: history_metadata?.persistState ? history_metadata.state : {},
534
+ persist_state: history_metadata?.persistState ?? false
437
535
  });
438
536
 
439
537
  restore_scroll();
@@ -482,6 +580,7 @@ async function _invalidate(reset_page_state = true) {
482
580
  }
483
581
 
484
582
  const prev_state = page.state;
583
+ const prev_shallow = page.shallow;
485
584
  const navigation_result = intent && (await load_route(intent));
486
585
  if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
487
586
  return;
@@ -490,7 +589,7 @@ async function _invalidate(reset_page_state = true) {
490
589
  if (navigation_result.type === 'redirect') {
491
590
  return _goto(
492
591
  new URL(navigation_result.location, current.url).href,
493
- { replaceState: true },
592
+ { replace: true },
494
593
  1,
495
594
  token
496
595
  );
@@ -506,11 +605,10 @@ async function _invalidate(reset_page_state = true) {
506
605
  if (!reset_page_state) {
507
606
  navigation_result.props.page.state = prev_state;
508
607
  }
509
- update(navigation_result.props.page);
510
- current_tree = navigation_result.props.tree;
608
+ navigation_result.props.page.shallow = prev_shallow;
609
+ apply_navigation_result(navigation_result);
511
610
  current = { ...navigation_result.state, nav: current.nav };
512
611
  reset_invalidation();
513
- root.$set(navigation_result.props);
514
612
 
515
613
  // only wait for promises that are connected to queries that still exist
516
614
  /** @type {Promise<any>[]} */
@@ -541,21 +639,21 @@ function reset_invalidation() {
541
639
 
542
640
  /** @param {number} index */
543
641
  function capture_snapshot(index) {
544
- if (components.some((c) => c?.snapshot)) {
545
- snapshots[index] = components.map((c) => c?.snapshot?.capture());
642
+ if (props.components.some((c) => c?.snapshot)) {
643
+ snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
546
644
  }
547
645
  }
548
646
 
549
647
  /** @param {number} index */
550
648
  function restore_snapshot(index) {
551
649
  snapshots[index]?.forEach((value, i) => {
552
- components[i]?.snapshot?.restore(value);
650
+ props.components[i]?.snapshot?.restore(value);
553
651
  });
554
652
  }
555
653
 
556
654
  function persist_state() {
557
- update_scroll_positions(current_history_index);
558
- storage.set(SCROLL_KEY, scroll_positions);
655
+ capture_scroll(current_history_index);
656
+ storage.set(HISTORY_INFO_KEY, history_info);
559
657
 
560
658
  capture_snapshot(current_navigation_index);
561
659
  storage.set(SNAPSHOT_KEY, snapshots);
@@ -563,7 +661,7 @@ function persist_state() {
563
661
 
564
662
  /**
565
663
  * @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
664
+ * @param {{ replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean }} options
567
665
  * @param {number} redirect_count
568
666
  * @param {{}} [nav_token]
569
667
  * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
@@ -584,10 +682,10 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
584
682
  await navigate({
585
683
  type: 'goto',
586
684
  url: resolve_url(url),
587
- keepfocus: options.keepFocus,
588
- noscroll: options.noScroll,
589
- replace_state: options.replaceState,
685
+ reset: options.reset,
686
+ replace_state: options.replace,
590
687
  state: options.state,
688
+ persist_state: options.persistState,
591
689
  redirect_count,
592
690
  nav_token,
593
691
  intent,
@@ -621,9 +719,8 @@ export async function _goto(url, options, redirect_count, nav_token, intent) {
621
719
  if (options.refreshAll) {
622
720
  // TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
623
721
  // 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)
722
+ void tick()
723
+ .then(tick)
627
724
  .then(() => {
628
725
  for (const [id, entries] of query_map) {
629
726
  for (const [payload, { resource }] of entries) {
@@ -665,7 +762,7 @@ async function _preload_data(intent) {
665
762
 
666
763
  load_cache.promise.catch(discard_load_cache);
667
764
 
668
- if (__SVELTEKIT_FORK_PRELOADS__ && svelte.fork) {
765
+ if (__SVELTEKIT_FORK_PRELOADS__) {
669
766
  const lc = load_cache;
670
767
 
671
768
  lc.fork = lc.promise.then((result) => {
@@ -673,10 +770,8 @@ async function _preload_data(intent) {
673
770
  // resolve, bail rather than creating an orphan fork
674
771
  if (lc === load_cache && result.type === 'loaded') {
675
772
  try {
676
- return svelte.fork(() => {
677
- root.$set(result.props);
678
- update(result.props.page);
679
- current_tree = result.props.tree;
773
+ return fork(() => {
774
+ apply_navigation_result(result);
680
775
  });
681
776
  } catch {
682
777
  // if it errors, it's because the experimental flag isn't enabled in Svelte
@@ -710,9 +805,9 @@ async function _preload_code(url) {
710
805
  /**
711
806
  * @param {import('./types.js').NavigationFinished} result
712
807
  * @param {HTMLElement} target
713
- * @param {boolean} hydrate
808
+ * @param {boolean} should_hydrate
714
809
  */
715
- async function initialize(result, target, hydrate) {
810
+ async function initialize(result, target, should_hydrate) {
716
811
  if (__SVELTEKIT_DEV__ && result.state.error && document.querySelector('vite-error-overlay'))
717
812
  return;
718
813
 
@@ -734,21 +829,18 @@ async function initialize(result, target, hydrate) {
734
829
  if (style) style.remove();
735
830
  }
736
831
 
737
- update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page));
738
- current_tree = result.props.tree;
832
+ apply_navigation_result(result);
739
833
 
740
- // TODO: use mount()
741
- root = new Root({
834
+ // TODO treeshake `hydrate` in csr mode
835
+ const render = should_hydrate ? hydrate : mount;
836
+
837
+ render(Root, {
742
838
  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,
839
+ props,
747
840
  transformError: /** @param {unknown} e */ async (e) => {
748
841
  const error = await handle_error(e, current.nav);
749
- rendering_error = { error, status: error.status };
750
842
  page.error = error;
751
- page.status = rendering_error.status;
843
+ page.status = error.status;
752
844
  return error;
753
845
  }
754
846
  });
@@ -757,16 +849,17 @@ async function initialize(result, target, hydrate) {
757
849
  // which causes component script blocks to run asynchronously
758
850
  void (await Promise.resolve());
759
851
 
760
- if (hydrate) {
852
+ if (should_hydrate) {
761
853
  /** @type {import('@sveltejs/kit').AfterNavigate} */
762
854
  const navigation = {
763
855
  from: null,
764
856
  to: {
765
857
  ...nav,
766
- scroll: scroll_positions[current_history_index] ?? scroll_state()
858
+ scroll: history_info[current_history_index]?.scroll ?? scroll_state()
767
859
  },
768
860
  willUnload: false,
769
861
  type: 'enter',
862
+ shallow: false,
770
863
  complete: Promise.resolve()
771
864
  };
772
865
 
@@ -848,7 +941,7 @@ async function get_navigation_result_from_branch({
848
941
  let current_node = result.props.tree;
849
942
 
850
943
  /** @type {RenderNode | undefined} */
851
- let previous_node = current_tree;
944
+ let previous_node = props.tree;
852
945
 
853
946
  for (let i = 0; i < branch.length; i += 1) {
854
947
  const node = branch[i];
@@ -856,11 +949,6 @@ async function get_navigation_result_from_branch({
856
949
 
857
950
  if (!node) continue;
858
951
 
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
952
  if (
865
953
  // if an ancestor node in this path changed, `data_changed` is already true and the
866
954
  // accumulated `data` differs from the previous render, so we must re-merge
@@ -878,10 +966,24 @@ async function get_navigation_result_from_branch({
878
966
  data = current_node.data;
879
967
 
880
968
  if (i < branch.length - 1) {
881
- current_node.child = /** @type {import('../types.js').RenderNode} */ ({});
882
- current_node = current_node.child;
969
+ let next_index = i + 1;
970
+ while (next_index < branch.length && !branch[next_index]) {
971
+ next_index += 1;
972
+ }
973
+
974
+ const next = branch[next_index];
975
+
976
+ if (next) {
977
+ const error_loader =
978
+ errors?.slice(0, next_index + 1).findLast((x) => x) ?? default_error_loader;
979
+
980
+ current_node = current_node.child = new RenderNode(
981
+ next.node.component,
982
+ (await error_loader()).component
983
+ );
883
984
 
884
- previous_node = previous_node?.child;
985
+ previous_node = previous_node?.child;
986
+ }
885
987
  }
886
988
  }
887
989
 
@@ -900,6 +1002,7 @@ async function get_navigation_result_from_branch({
900
1002
  id: route?.id ?? null
901
1003
  },
902
1004
  state: {},
1005
+ shallow: null,
903
1006
  status: status ?? error?.status ?? 200,
904
1007
  url: new URL(url),
905
1008
  form: form ?? null,
@@ -1298,7 +1401,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1298
1401
 
1299
1402
  if (server_data_node?.type === 'error') {
1300
1403
  // rethrow and catch below
1301
- throw new HttpError(server_data_node.error.status, server_data_node.error);
1404
+ throw new HttpError(server_data_node.error);
1302
1405
  }
1303
1406
 
1304
1407
  return load_node({
@@ -1655,12 +1758,14 @@ function get_page_key(url) {
1655
1758
  * delta?: number;
1656
1759
  * event?: PopStateEvent | MouseEvent;
1657
1760
  * scroll?: { x: number, y: number };
1761
+ * shallow?: boolean;
1762
+ * target?: { params: Record<string, string> | null; route: { id: string } | null; url: URL };
1658
1763
  * }} opts
1659
1764
  */
1660
- function _before_navigate({ url, type, intent, delta, event, scroll }) {
1765
+ function _before_navigate({ url, type, intent, delta, event, scroll, shallow = false, target }) {
1661
1766
  let should_block = false;
1662
1767
 
1663
- const nav = create_navigation(current, intent, url, type, scroll ?? null);
1768
+ const nav = create_navigation(current, intent, url, type, scroll ?? null, shallow, target);
1664
1769
 
1665
1770
  if (nav.navigation.type === 'popstate' && delta !== undefined) {
1666
1771
  nav.navigation.delta = delta;
@@ -1681,6 +1786,9 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1681
1786
 
1682
1787
  if (!is_navigating) {
1683
1788
  // Don't run the event during redirects
1789
+ // TODO this isn't fully right: if you do a goto(...) while another goto(...) is in progress,
1790
+ // or you click a link while a navigation is in progress, the beforNavigate calls are not triggered,
1791
+ // and maybe they should be?
1684
1792
  before_navigate_callbacks.forEach((fn) => fn(cancellable));
1685
1793
  }
1686
1794
 
@@ -1693,13 +1801,14 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1693
1801
  * url: URL;
1694
1802
  * popped?: {
1695
1803
  * state: Record<string, any>;
1696
- * scroll: { x: number, y: number };
1804
+ * scroll?: { x: number, y: number };
1697
1805
  * delta: number;
1806
+ * shallow: { params: Record<string, string> | null; route: { id: string } | null; url: URL } | null;
1698
1807
  * };
1699
- * keepfocus?: boolean;
1700
- * noscroll?: boolean;
1808
+ * reset?: boolean;
1701
1809
  * replace_state?: boolean;
1702
1810
  * state?: Record<string, any>;
1811
+ * persist_state?: boolean;
1703
1812
  * redirect_count?: number;
1704
1813
  * nav_token?: {};
1705
1814
  * accept?: () => void;
@@ -1713,10 +1822,10 @@ async function navigate({
1713
1822
  type,
1714
1823
  url,
1715
1824
  popped,
1716
- keepfocus,
1717
- noscroll,
1825
+ reset = true,
1718
1826
  replace_state,
1719
1827
  state = {},
1828
+ persist_state = false,
1720
1829
  redirect_count = 0,
1721
1830
  nav_token = {},
1722
1831
  accept = noop,
@@ -1738,6 +1847,8 @@ async function navigate({
1738
1847
  delta: popped?.delta,
1739
1848
  intent,
1740
1849
  scroll: popped?.scroll,
1850
+ shallow: !!popped?.shallow,
1851
+ target: popped?.shallow ?? undefined,
1741
1852
  // @ts-ignore
1742
1853
  event
1743
1854
  });
@@ -1821,10 +1932,10 @@ async function navigate({
1821
1932
  type,
1822
1933
  url: new URL(navigation_result.location, url),
1823
1934
  popped,
1824
- keepfocus,
1825
- noscroll,
1935
+ reset,
1826
1936
  replace_state,
1827
1937
  state,
1938
+ persist_state,
1828
1939
  redirect_count: redirect_count + 1,
1829
1940
  nav_token
1830
1941
  });
@@ -1858,7 +1969,7 @@ async function navigate({
1858
1969
 
1859
1970
  updating = true;
1860
1971
 
1861
- update_scroll_positions(previous_history_index);
1972
+ capture_scroll(previous_history_index);
1862
1973
  capture_snapshot(previous_navigation_index);
1863
1974
 
1864
1975
  // ensure the url pathname matches the page's trailing slash option
@@ -1871,15 +1982,23 @@ async function navigate({
1871
1982
  if (!popped) {
1872
1983
  // this is a new navigation, rather than a popstate
1873
1984
  const change = replace_state ? 0 : 1;
1985
+ if (type !== 'enter') {
1986
+ if (reset) current_reset_index += 1;
1987
+ }
1874
1988
 
1875
1989
  const entry = {
1876
- [HISTORY_INDEX]: (current_history_index += change),
1877
- [NAVIGATION_INDEX]: (current_navigation_index += change),
1878
- [STATES_KEY]: state
1990
+ [HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
1991
+ historyIndex: (current_history_index += change),
1992
+ navigationIndex: (current_navigation_index += change),
1993
+ state,
1994
+ persistState: persist_state,
1995
+ resetIndex: current_reset_index
1996
+ })
1879
1997
  };
1880
1998
 
1881
1999
  const fn = replace_state ? history.replaceState : history.pushState;
1882
2000
  fn.call(history, entry, '', url);
2001
+ set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
1883
2002
 
1884
2003
  if (!replace_state) {
1885
2004
  clear_onward_history(current_history_index, current_navigation_index);
@@ -1898,6 +2017,7 @@ async function navigate({
1898
2017
  }
1899
2018
 
1900
2019
  navigation_result.props.page.state = state;
2020
+ navigation_result.props.page.shallow = popped?.shallow ?? null;
1901
2021
 
1902
2022
  /**
1903
2023
  * @type {Promise<void> | undefined}
@@ -1928,7 +2048,13 @@ async function navigate({
1928
2048
  }
1929
2049
 
1930
2050
  // Type-casts are save because we know this resolved a proper SvelteKit route
1931
- const target = /** @type {import('@sveltejs/kit').NavigationTarget} */ (nav.navigation.to);
2051
+ const target = popped?.shallow
2052
+ ? {
2053
+ params: navigation_result.state.params,
2054
+ route: { id: navigation_result.state.route?.id ?? null },
2055
+ url: navigation_result.state.url
2056
+ }
2057
+ : /** @type {import('@sveltejs/kit').NavigationTarget} */ (nav.navigation.to);
1932
2058
  current = {
1933
2059
  ...navigation_result.state,
1934
2060
  nav: {
@@ -1945,13 +2071,7 @@ async function navigate({
1945
2071
 
1946
2072
  // Remove focus before updating the component tree, so that blur/focusout
1947
2073
  // 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
- }
2074
+ blur_active_element(reset);
1955
2075
 
1956
2076
  const fork = load_cache_fork && (await load_cache_fork);
1957
2077
 
@@ -1960,28 +2080,22 @@ async function navigate({
1960
2080
  // `fork.commit()` applies the preloaded state synchronously before the
1961
2081
  // first `await`, so reset any previously-failed boundaries now so the
1962
2082
  // stale `+error.svelte` is torn down. See sveltejs/kit#15694.
1963
- for (const reset of resetters) {
1964
- reset?.();
2083
+ for (const reset_boundary of resetters) {
2084
+ reset_boundary();
1965
2085
  }
1966
- resetters.length = 0;
2086
+ resetters.clear();
1967
2087
  } 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;
2088
+ apply_navigation_result(navigation_result);
2089
+
1971
2090
  // Reset any boundaries that failed on a previous navigation now that the
1972
2091
  // new props are applied, otherwise the stale `+error.svelte` stays
1973
2092
  // mounted above the new route's content. See sveltejs/kit#15694.
1974
- for (const reset of resetters) {
1975
- reset?.();
2093
+ for (const reset_boundary of resetters) {
2094
+ reset_boundary();
1976
2095
  }
1977
- resetters.length = 0;
1978
- // Check for sync rendering error
1979
- if (rendering_error) {
1980
- Object.assign(navigation_result.props.page, rendering_error);
1981
- }
1982
- update(navigation_result.props.page);
2096
+ resetters.clear();
1983
2097
 
1984
- commit_promise = svelte.settled?.();
2098
+ commit_promise = settled();
1985
2099
  }
1986
2100
 
1987
2101
  has_navigated = true;
@@ -1991,7 +2105,7 @@ async function navigate({
1991
2105
 
1992
2106
  const { activeElement } = document;
1993
2107
 
1994
- await (commit_promise ?? svelte.tick());
2108
+ await commit_promise;
1995
2109
 
1996
2110
  if (navigation_token !== nav_token) {
1997
2111
  // a new navigation happened while we were waiting for the DOM to update, so abort
@@ -1999,44 +2113,7 @@ async function navigate({
1999
2113
  return;
2000
2114
  }
2001
2115
 
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;
2116
+ reset_scroll_and_focus(url, reset ? popped?.scroll : scroll_state(), reset, activeElement);
2040
2117
 
2041
2118
  is_navigating = false;
2042
2119
 
@@ -2280,7 +2357,7 @@ export async function handle_error(error, event) {
2280
2357
  * @param {T} callback
2281
2358
  */
2282
2359
  function add_navigation_callback(callbacks, callback) {
2283
- svelte.onMount(() => {
2360
+ onMount(() => {
2284
2361
  callbacks.add(callback);
2285
2362
 
2286
2363
  return () => {
@@ -2352,24 +2429,49 @@ export function disableScrollHandling() {
2352
2429
  }
2353
2430
 
2354
2431
  let warned_on_invalidate_all = false;
2432
+ let warned_on_replace_state = false;
2433
+ let warned_on_push_state = false;
2434
+ let warned_on_replace_state_function = false;
2435
+
2436
+ /**
2437
+ * @param {string | URL} url
2438
+ * @param {'goto' | 'pushState' | 'replaceState'} caller
2439
+ */
2440
+ async function resolve_intent(url, caller) {
2441
+ const resolved = new URL(resolve_url(url));
2442
+
2443
+ if (resolved.origin !== origin) {
2444
+ throw new Error(
2445
+ DEV
2446
+ ? `Cannot use \`${caller}\` with an external URL. Use \`window.location = "${url}"\` instead`
2447
+ : `${caller}: invalid URL`
2448
+ );
2449
+ }
2450
+
2451
+ const intent = await get_navigation_intent(resolved, false);
2452
+
2453
+ if (!intent) {
2454
+ throw new Error(
2455
+ DEV
2456
+ ? `Cannot use \`${caller}\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
2457
+ : `${caller}: invalid URL`
2458
+ );
2459
+ }
2460
+
2461
+ return intent;
2462
+ }
2355
2463
 
2356
2464
  /**
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`.
2465
+ * Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
2466
+ * (as they would be with a regular navigation) or preserved.
2467
+ *
2468
+ * 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.
2359
2469
  *
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.
2470
+ * `goto` is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
2362
2471
  * For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
2363
2472
  *
2364
2473
  * @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`
2474
+ * @param {import('@sveltejs/kit').GotoOptions} [opts] Options related to the navigation
2373
2475
  * @returns {Promise<void>}
2374
2476
  */
2375
2477
  export async function goto(url, opts = {}) {
@@ -2377,23 +2479,35 @@ export async function goto(url, opts = {}) {
2377
2479
  throw new Error('Cannot call goto(...) on the server');
2378
2480
  }
2379
2481
 
2380
- url = new URL(resolve_url(url));
2482
+ if (DEV) {
2483
+ if ('replaceState' in opts && !warned_on_replace_state) {
2484
+ warned_on_replace_state = true;
2485
+ console.warn(
2486
+ `The \`goto(..., { replaceState: ${opts.replaceState} })\` option has been deprecated in favour of \`replace\``
2487
+ );
2488
+ }
2381
2489
 
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
- );
2490
+ if ('noScroll' in opts || 'keepFocus' in opts) {
2491
+ throw new Error(
2492
+ `The \`goto(..., { noScroll: true, keepFocus: true })\` options have been replaced by \`reset: false\``
2493
+ );
2494
+ }
2388
2495
  }
2389
2496
 
2390
- const intent = await get_navigation_intent(url, false);
2497
+ const replace = opts.replace ?? opts.replaceState ?? false;
2391
2498
 
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'
2499
+ const intent = await resolve_intent(url, 'goto');
2500
+
2501
+ if (opts.shallow) {
2502
+ return update_state(
2503
+ intent,
2504
+ opts.state ?? {},
2505
+ {
2506
+ replace,
2507
+ persist_state: opts.persistState ?? false,
2508
+ reset: opts.reset ?? false
2509
+ },
2510
+ 'goto'
2397
2511
  );
2398
2512
  }
2399
2513
 
@@ -2404,8 +2518,13 @@ export async function goto(url, opts = {}) {
2404
2518
  );
2405
2519
  }
2406
2520
 
2407
- opts.refreshAll = opts.refreshAll ?? opts.invalidateAll;
2408
- return _goto(url, opts, 0, {}, intent);
2521
+ return _goto(
2522
+ intent.url,
2523
+ { ...opts, replace, refreshAll: opts.refreshAll ?? opts.invalidateAll },
2524
+ 0,
2525
+ {},
2526
+ intent
2527
+ );
2409
2528
  }
2410
2529
 
2411
2530
  /**
@@ -2579,63 +2698,77 @@ export async function preloadCode(pathname) {
2579
2698
  }
2580
2699
 
2581
2700
  /**
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).
2701
+ * Programmatically create a new history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2583
2702
  *
2703
+ * @deprecated Use `goto(url, { state, shallow: true })` instead.
2584
2704
  * @param {string | URL} url
2585
2705
  * @param {App.PageState} state
2586
- * @returns {void}
2706
+ * @returns {Promise<void>}
2587
2707
  */
2588
- export function pushState(url, state) {
2708
+ export async function pushState(url, state) {
2589
2709
  if (!BROWSER) {
2590
2710
  throw new Error('Cannot call pushState(...) on the server');
2591
2711
  }
2592
2712
 
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
- }
2713
+ if (DEV && !warned_on_push_state) {
2714
+ warned_on_push_state = true;
2715
+ console.warn(
2716
+ '`pushState(...)` is deprecated. Use `goto(url, { state, shallow: true })` instead.'
2717
+ );
2605
2718
  }
2606
2719
 
2607
- update_scroll_positions(current_history_index);
2720
+ const intent = await resolve_intent(url, 'pushState');
2608
2721
 
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);
2722
+ await update_state(
2723
+ intent,
2724
+ state,
2725
+ { replace: false, persist_state: false, reset: false },
2726
+ 'pushState'
2727
+ );
2622
2728
  }
2623
2729
 
2624
2730
  /**
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).
2731
+ * Programmatically replace the current history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2626
2732
  *
2733
+ * @deprecated Use `goto(url, { state, shallow: true, replace: true })` instead.
2627
2734
  * @param {string | URL} url
2628
2735
  * @param {App.PageState} state
2629
- * @returns {void}
2736
+ * @returns {Promise<void>}
2630
2737
  */
2631
- export function replaceState(url, state) {
2738
+ export async function replaceState(url, state) {
2632
2739
  if (!BROWSER) {
2633
2740
  throw new Error('Cannot call replaceState(...) on the server');
2634
2741
  }
2635
2742
 
2743
+ if (DEV && !warned_on_replace_state_function) {
2744
+ warned_on_replace_state_function = true;
2745
+ console.warn(
2746
+ '`replaceState(...)` is deprecated. Use `goto(url, { state, shallow: true, replace: true })` instead.'
2747
+ );
2748
+ }
2749
+
2750
+ const intent = await resolve_intent(url, 'replaceState');
2751
+
2752
+ await update_state(
2753
+ intent,
2754
+ state,
2755
+ { replace: true, persist_state: false, reset: false },
2756
+ 'replaceState'
2757
+ );
2758
+ }
2759
+
2760
+ /**
2761
+ * @param {NavigationIntent} intent
2762
+ * @param {App.PageState} state
2763
+ * @param {{ replace: boolean; persist_state: boolean; reset: boolean; }} options
2764
+ * @param {'goto' | 'pushState' | 'replaceState'} caller
2765
+ */
2766
+ async function update_state(intent, state, { replace, persist_state, reset }, caller) {
2767
+ const url = intent.url;
2768
+
2636
2769
  if (DEV) {
2637
2770
  if (!started) {
2638
- throw new Error('Cannot call replaceState(...) before router is initialized');
2771
+ throw new Error(`Cannot call ${caller}(...) before router is initialized`);
2639
2772
  }
2640
2773
 
2641
2774
  try {
@@ -2647,16 +2780,100 @@ export function replaceState(url, state) {
2647
2780
  }
2648
2781
  }
2649
2782
 
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
2783
+ const nav =
2784
+ // For backwards compatibility we don't trigger navigation hooks etc for push/replaceState
2785
+ caller === 'goto' ? _before_navigate({ url, type: 'goto', intent, shallow: true }) : undefined;
2786
+
2787
+ if (!nav && caller === 'goto') return;
2788
+
2789
+ const nav_token = {};
2790
+
2791
+ if (nav) {
2792
+ navigation_token = invalidation_token = nav_token;
2793
+ is_navigating = true;
2794
+ navigating.current = nav.navigation;
2795
+ updating = true;
2796
+ }
2797
+
2798
+ if (!replace) capture_scroll(current_history_index);
2799
+ if (reset) current_reset_index += 1;
2800
+
2801
+ const entry = {
2802
+ [HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
2803
+ historyIndex: (current_history_index += replace ? 0 : 1),
2804
+ navigationIndex: current_navigation_index,
2805
+ pageUrl: page.url.href,
2806
+ state,
2807
+ persistState: persist_state,
2808
+ resetIndex: current_reset_index
2809
+ })
2655
2810
  };
2656
2811
 
2657
- history.replaceState(opts, '', resolve_url(url));
2812
+ const fn = replace ? history.replaceState : history.pushState;
2813
+ fn.call(history, entry, '', url);
2814
+ set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
2815
+
2816
+ if (!replace) {
2817
+ has_navigated = true;
2818
+ clear_onward_history(current_history_index, current_navigation_index);
2819
+ }
2820
+
2821
+ if (nav) {
2822
+ const after_navigate = (
2823
+ await Promise.all(
2824
+ // eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
2825
+ Array.from(on_navigate_callbacks, (fn) =>
2826
+ fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
2827
+ )
2828
+ )
2829
+ ).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
2830
+
2831
+ if (after_navigate.length > 0) {
2832
+ function cleanup() {
2833
+ after_navigate.forEach((fn) => after_navigate_callbacks.delete(fn));
2834
+ }
2835
+
2836
+ after_navigate.push(cleanup);
2837
+ after_navigate.forEach((fn) => after_navigate_callbacks.add(fn));
2838
+ }
2839
+ }
2840
+
2841
+ blur_active_element(reset);
2658
2842
 
2659
2843
  page.state = state;
2844
+ page.shallow = {
2845
+ params: intent?.params ?? null,
2846
+ route: intent ? { id: intent.route.id } : null,
2847
+ url
2848
+ };
2849
+
2850
+ if (!nav) return;
2851
+
2852
+ const { activeElement } = document;
2853
+
2854
+ await settled();
2855
+
2856
+ if (navigation_token !== nav_token) {
2857
+ // a new navigation happened while we were waiting for the DOM to update, so abort
2858
+ nav.reject(new Error('navigation aborted'));
2859
+ return;
2860
+ }
2861
+
2862
+ reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
2863
+
2864
+ is_navigating = false;
2865
+ nav.fulfil(undefined);
2866
+
2867
+ if (nav.navigation.to) {
2868
+ nav.navigation.to.scroll = scroll_state();
2869
+ }
2870
+
2871
+ after_navigate_callbacks.forEach((fn) =>
2872
+ fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
2873
+ );
2874
+
2875
+ navigating.current = null;
2876
+ updating = false;
2660
2877
  }
2661
2878
 
2662
2879
  /**
@@ -2681,15 +2898,13 @@ export async function applyAction(result) {
2681
2898
  page.status = result.status;
2682
2899
 
2683
2900
  /** @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
- });
2901
+ // this brings Svelte's view of the world in line with SvelteKit's
2902
+ // after use:enhance reset the form....
2903
+ props.form = null;
2689
2904
 
2690
2905
  // ...so that setting the `form` prop takes effect and isn't ignored
2691
- await svelte.tick();
2692
- root.$set({ form: result.data });
2906
+ await tick();
2907
+ props.form = result.data;
2693
2908
 
2694
2909
  if (result.type === 'success') {
2695
2910
  reset_focus(/** @type {URL} */ (page.url));
@@ -2719,11 +2934,9 @@ export async function set_nearest_error_page(error) {
2719
2934
 
2720
2935
  current = { ...navigation_result.state, nav: current.nav };
2721
2936
 
2722
- root.$set(navigation_result.props);
2723
- current_tree = navigation_result.props.tree;
2724
- update(navigation_result.props.page);
2937
+ apply_navigation_result(navigation_result);
2725
2938
 
2726
- void svelte.tick().then(() => reset_focus(current.url));
2939
+ void tick().then(() => reset_focus(current.url));
2727
2940
  }
2728
2941
  }
2729
2942
 
@@ -2866,17 +3079,21 @@ function _start_router() {
2866
3079
  return;
2867
3080
  }
2868
3081
  // set this flag to distinguish between navigations triggered by
2869
- // clicking a hash link and those triggered by popstate
2870
- hash_navigating = true;
3082
+ // clicking a hash link and those triggered by popstate. We gotta retrieve
3083
+ // history metadata here because the hashchange event will occur after history.state was updated
3084
+ hash_navigating = {
3085
+ .../** @type {HistoryMetadata} */ (get_history_metadata()),
3086
+ resetIndex: current_reset_index + (options.reset ? 1 : 0)
3087
+ };
2871
3088
 
2872
- update_scroll_positions(current_history_index);
3089
+ capture_scroll(current_history_index);
2873
3090
 
2874
3091
  update_url(url);
2875
3092
 
2876
3093
  if (!options.replace_state) return;
2877
3094
 
2878
3095
  // hashchange event shouldn't occur if the router is replacing state.
2879
- hash_navigating = false;
3096
+ hash_navigating = null;
2880
3097
  }
2881
3098
 
2882
3099
  event.preventDefault();
@@ -2894,8 +3111,7 @@ function _start_router() {
2894
3111
  await navigate({
2895
3112
  type: 'link',
2896
3113
  url,
2897
- keepfocus: options.keepfocus,
2898
- noscroll: options.noscroll,
3114
+ reset: options.reset,
2899
3115
  replace_state: options.replace_state ?? url.href === location.href,
2900
3116
  event
2901
3117
  });
@@ -2941,8 +3157,7 @@ function _start_router() {
2941
3157
  void navigate({
2942
3158
  type: 'form',
2943
3159
  url,
2944
- keepfocus: options.keepfocus,
2945
- noscroll: options.noscroll,
3160
+ reset: options.reset,
2946
3161
  replace_state: options.replace_state ?? url.href === location.href,
2947
3162
  event
2948
3163
  });
@@ -2951,53 +3166,82 @@ function _start_router() {
2951
3166
  addEventListener('popstate', async (event) => {
2952
3167
  if (resetting_focus) return;
2953
3168
 
2954
- if (event.state?.[HISTORY_INDEX]) {
2955
- const history_index = event.state[HISTORY_INDEX];
3169
+ const history_metadata = get_history_metadata(event.state);
3170
+
3171
+ if (history_metadata?.historyIndex) {
3172
+ const history_index = history_metadata.historyIndex;
3173
+ const source_info = history_info[current_history_index];
2956
3174
  navigation_token = invalidation_token = {};
2957
3175
 
2958
3176
  // if a popstate-driven navigation is cancelled, we need to counteract it
2959
3177
  // with history.go, which means we end up back here, hence this check
2960
3178
  if (history_index === current_history_index) return;
2961
3179
 
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;
3180
+ const delta = history_index - current_history_index;
3181
+ const reset_index = history_metadata.resetIndex;
3182
+ const reset = reset_index !== (source_info?.resetIndex ?? current_reset_index);
3183
+ const scroll = history_info[history_index]?.scroll;
3184
+ const state = history_metadata.state;
3185
+ const url = new URL(history_metadata.pageUrl ?? location.href);
3186
+ const navigation_index = history_metadata.navigationIndex;
3187
+ const is_hash_change =
3188
+ current.url && (location.href + current.url.href).includes('#') // check if even has a hash
3189
+ ? strip_hash(location) === strip_hash(current.url)
3190
+ : false;
2967
3191
  const shallow =
2968
- navigation_index === current_navigation_index && (has_navigated || is_hash_change);
3192
+ navigation_index === current_navigation_index &&
3193
+ ((has_navigated &&
3194
+ (history_metadata.pageUrl === undefined || history_metadata.pageUrl === location.href)) ||
3195
+ is_hash_change);
3196
+ const shallow_url = history_metadata.pageUrl ? new URL(location.href) : null;
3197
+ const shallow_intent = shallow_url
3198
+ ? await get_navigation_intent(shallow_url, false)
3199
+ : undefined;
3200
+ const shallow_target = shallow_url
3201
+ ? {
3202
+ params: shallow_intent?.params ?? null,
3203
+ route: shallow_intent ? { id: shallow_intent.route.id } : null,
3204
+ url: shallow_url
3205
+ }
3206
+ : null;
2969
3207
 
2970
3208
  if (shallow) {
2971
3209
  // We don't need to navigate, we just need to update scroll and/or state.
2972
3210
  // This happens with hash links and `pushState`/`replaceState`. The
2973
3211
  // exception is if we haven't navigated yet, since we could have
2974
3212
  // got here after a modal navigation then a reload
3213
+
3214
+ blur_active_element(reset);
3215
+
2975
3216
  if (state !== page.state) {
2976
3217
  page.state = state;
2977
3218
  }
2978
3219
 
2979
- update_url(url);
3220
+ page.shallow = shallow_target;
2980
3221
 
2981
- scroll_positions[current_history_index] = scroll_state();
2982
- if (scroll) scrollTo(scroll.x, scroll.y);
3222
+ update_url(url);
2983
3223
 
3224
+ capture_scroll(current_history_index);
2984
3225
  current_history_index = history_index;
3226
+ current_reset_index = reset_index;
3227
+ if (reset && scroll) scrollTo(scroll.x, scroll.y);
2985
3228
  return;
2986
3229
  }
2987
3230
 
2988
- const delta = history_index - current_history_index;
2989
-
2990
3231
  await navigate({
2991
3232
  type: 'popstate',
2992
3233
  url,
3234
+ reset,
2993
3235
  popped: {
2994
3236
  state,
2995
3237
  scroll,
2996
- delta
3238
+ delta,
3239
+ shallow: shallow_target
2997
3240
  },
2998
3241
  accept: () => {
2999
3242
  current_history_index = history_index;
3000
3243
  current_navigation_index = navigation_index;
3244
+ current_reset_index = reset_index;
3001
3245
  },
3002
3246
  block: () => {
3003
3247
  history.go(-delta);
@@ -3026,16 +3270,22 @@ function _start_router() {
3026
3270
  // if the hashchange happened as a result of clicking on a link,
3027
3271
  // we need to update history, otherwise we have to leave it alone
3028
3272
  if (hash_navigating) {
3029
- hash_navigating = false;
3273
+ const history_metadata = hash_navigating;
3274
+ hash_navigating = null;
3275
+ current_reset_index = history_metadata.resetIndex;
3030
3276
  history.replaceState(
3031
3277
  {
3032
3278
  ...history.state,
3033
- [HISTORY_INDEX]: ++current_history_index,
3034
- [NAVIGATION_INDEX]: current_navigation_index
3279
+ [HISTORY_METADATA_KEY]: {
3280
+ ...history_metadata,
3281
+ historyIndex: ++current_history_index,
3282
+ navigationIndex: current_navigation_index
3283
+ }
3035
3284
  },
3036
3285
  '',
3037
3286
  location.href
3038
3287
  );
3288
+ set_history_options(current_history_index, history_metadata);
3039
3289
  }
3040
3290
  });
3041
3291
 
@@ -3102,7 +3352,7 @@ async function _hydrate(
3102
3352
 
3103
3353
  /** @type {import('./types.js').NavigationFinished | undefined} */
3104
3354
  let result;
3105
- let hydrate = true;
3355
+ let should_hydrate = true;
3106
3356
 
3107
3357
  try {
3108
3358
  const branch_promises = node_ids.map(async (n, i) => {
@@ -3168,7 +3418,7 @@ async function _hydrate(
3168
3418
  });
3169
3419
 
3170
3420
  target.textContent = '';
3171
- hydrate = false;
3421
+ should_hydrate = false;
3172
3422
  }
3173
3423
 
3174
3424
  // Exit early when we encounter a redirect while loading the root error page.
@@ -3176,10 +3426,11 @@ async function _hydrate(
3176
3426
  if (!result) return;
3177
3427
 
3178
3428
  if (result.props.page) {
3179
- result.props.page.state = {};
3429
+ const history_metadata = get_history_metadata();
3430
+ result.props.page.state = history_metadata?.persistState ? history_metadata.state : {};
3180
3431
  }
3181
3432
 
3182
- await initialize(result, target, hydrate);
3433
+ await initialize(result, target, should_hydrate);
3183
3434
  }
3184
3435
 
3185
3436
  /**
@@ -3206,20 +3457,19 @@ async function load_data(url, invalid) {
3206
3457
  notify_version(res.headers.get('x-sveltekit-version'));
3207
3458
 
3208
3459
  if (!res.ok) {
3209
- // error message is a JSON-stringified string which devalue can't handle at the top level
3210
3460
  // turn it into a HttpError to not call handleError on the client again (was already handled on the server)
3211
3461
  // if `__data.json` doesn't exist or the server has an internal error,
3212
3462
  // avoid parsing the HTML error page as a JSON
3213
- /** @type {string | undefined} */
3214
- let message;
3463
+ /** @type {App.Error} */
3464
+ let error = { status: res.status, message: 'Internal Error' };
3465
+
3215
3466
  if (res.headers.get('content-type')?.includes('application/json')) {
3216
- message = await res.json();
3467
+ error = { status: res.status, ...(await res.json()) };
3217
3468
  } else if (res.status === 404) {
3218
- message = 'Not Found';
3219
- } else if (res.status === 500) {
3220
- message = 'Internal Error';
3469
+ error.message = 'Not Found';
3221
3470
  }
3222
- throw new HttpError(res.status, message);
3471
+
3472
+ throw new HttpError(error);
3223
3473
  }
3224
3474
 
3225
3475
  return new Promise((resolve, reject) => {
@@ -3412,8 +3662,18 @@ function reset_focus(url, scroll = true) {
3412
3662
  * @param {URL | null} url
3413
3663
  * @param {T} type
3414
3664
  * @param {{ x: number, y: number } | null} [target_scroll] The scroll position for the target (for popstate navigations)
3665
+ * @param {boolean} [shallow]
3666
+ * @param {{ params: Record<string, string> | null; route: { id: string } | null; url: URL }} [target]
3415
3667
  */
3416
- function create_navigation(current, intent, url, type, target_scroll = null) {
3668
+ function create_navigation(
3669
+ current,
3670
+ intent,
3671
+ url,
3672
+ type,
3673
+ target_scroll = null,
3674
+ shallow = false,
3675
+ target
3676
+ ) {
3417
3677
  /** @type {(value: any) => void} */
3418
3678
  let fulfil;
3419
3679
 
@@ -3437,13 +3697,14 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
3437
3697
  scroll: scroll_state()
3438
3698
  },
3439
3699
  to: url && {
3440
- params: intent?.params ?? null,
3441
- route: { id: intent?.route?.id ?? null },
3442
- url,
3700
+ params: target ? target.params : (intent?.params ?? null),
3701
+ route: { id: target ? (target.route?.id ?? null) : (intent?.route?.id ?? null) },
3702
+ url: target?.url ?? url,
3443
3703
  scroll: target_scroll
3444
3704
  },
3445
- willUnload: !intent,
3705
+ willUnload: !shallow && !intent,
3446
3706
  type,
3707
+ shallow,
3447
3708
  complete
3448
3709
  });
3449
3710
 
@@ -3516,3 +3777,21 @@ if (DEV) {
3516
3777
  });
3517
3778
  }
3518
3779
  }
3780
+
3781
+ /**
3782
+ * @param {NavigationFinished} result
3783
+ */
3784
+ function apply_navigation_result(result) {
3785
+ Object.assign(page, result.props.page);
3786
+
3787
+ props.tree.data = result.props.tree.data;
3788
+ props.tree.child = result.props.tree.child;
3789
+
3790
+ if ('form' in result.props) {
3791
+ props.form = result.props.form;
3792
+ }
3793
+
3794
+ if ('error' in result.props) {
3795
+ props.error = result.props.error;
3796
+ }
3797
+ }