@sveltejs/kit 3.0.0-next.2 → 3.0.0-next.21

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 (195) hide show
  1. package/package.json +62 -36
  2. package/src/cli.js +15 -12
  3. package/src/constants.js +6 -1
  4. package/src/core/adapt/builder.js +66 -72
  5. package/src/core/adapt/index.js +17 -6
  6. package/src/core/config/index.js +134 -80
  7. package/src/core/config/options.js +295 -273
  8. package/src/core/config/types.d.ts +1 -1
  9. package/src/core/env.js +146 -16
  10. package/src/core/generate_manifest/index.js +18 -15
  11. package/src/core/postbuild/analyse.js +8 -20
  12. package/src/core/postbuild/crawl.js +22 -6
  13. package/src/core/postbuild/entities.js +8 -2
  14. package/src/core/postbuild/fallback.js +4 -2
  15. package/src/core/postbuild/prerender.js +222 -70
  16. package/src/core/postbuild/queue.js +2 -1
  17. package/src/core/sync/create_manifest_data/conflict.js +1 -1
  18. package/src/core/sync/create_manifest_data/index.js +82 -62
  19. package/src/core/sync/sync.js +40 -31
  20. package/src/core/sync/ts.js +1 -1
  21. package/src/core/sync/utils.js +22 -2
  22. package/src/core/sync/{write_non_ambient.js → write_app_types.js} +107 -58
  23. package/src/core/sync/write_client_manifest.js +14 -29
  24. package/src/core/sync/write_env.js +7 -5
  25. package/src/core/sync/write_server.js +37 -45
  26. package/src/core/sync/write_tsconfig/index.js +274 -0
  27. package/src/core/sync/write_tsconfig/test-app/package.json +7 -0
  28. package/src/core/sync/write_tsconfig/utils.js +77 -0
  29. package/src/core/sync/write_tsconfig/validate.js +128 -0
  30. package/src/core/sync/write_types/index.js +127 -123
  31. package/src/core/utils.js +30 -5
  32. package/src/exports/env/index.js +77 -0
  33. package/src/exports/env/public.d.ts +55 -0
  34. package/src/exports/hooks/index.js +3 -9
  35. package/src/exports/hooks/public.d.ts +195 -0
  36. package/src/exports/hooks/sequence.js +9 -6
  37. package/src/exports/index.js +52 -29
  38. package/src/exports/internal/client.js +5 -0
  39. package/src/exports/internal/env.js +8 -5
  40. package/src/exports/internal/index.js +1 -90
  41. package/src/exports/internal/{event.js → server/event.js} +2 -3
  42. package/src/exports/internal/server/index.js +37 -0
  43. package/src/exports/internal/server/telemetry.js +95 -0
  44. package/src/exports/internal/shared.js +90 -0
  45. package/src/exports/node/index.js +64 -22
  46. package/src/exports/params/index.js +70 -0
  47. package/src/exports/params/public.d.ts +63 -0
  48. package/src/exports/public.d.ts +50 -1733
  49. package/src/exports/remote/index.js +11 -0
  50. package/src/exports/remote/public.d.ts +519 -0
  51. package/src/exports/url.js +86 -0
  52. package/src/exports/vite/build/build_server.js +54 -65
  53. package/src/exports/vite/build/remote.js +24 -19
  54. package/src/exports/vite/build/utils.js +0 -8
  55. package/src/exports/vite/dev/index.js +218 -146
  56. package/src/exports/vite/index.js +1590 -858
  57. package/src/exports/vite/module_ids.js +2 -2
  58. package/src/exports/vite/preview/index.js +40 -32
  59. package/src/exports/vite/public.d.ts +588 -0
  60. package/src/exports/vite/utils.js +84 -46
  61. package/src/pathname.js +55 -0
  62. package/src/runner.js +15 -0
  63. package/src/runtime/app/env/internal.js +4 -4
  64. package/src/runtime/app/env/types.d.ts +1 -1
  65. package/src/runtime/app/environment/index.js +3 -3
  66. package/src/runtime/app/{forms.js → forms/index.js} +73 -42
  67. package/src/runtime/app/forms/public.d.ts +2 -0
  68. package/src/runtime/app/forms/types.d.ts +56 -0
  69. package/src/runtime/app/internal/transport.js +53 -0
  70. package/src/runtime/app/manifest/index.js +1 -0
  71. package/src/runtime/app/{navigation.js → navigation/index.js} +2 -1
  72. package/src/runtime/app/navigation/public.d.ts +237 -0
  73. package/src/runtime/app/paths/client.js +37 -37
  74. package/src/runtime/app/paths/index.js +1 -1
  75. package/src/runtime/app/paths/internal/client.js +34 -2
  76. package/src/runtime/app/paths/internal/server.js +6 -23
  77. package/src/runtime/app/paths/internal.d.ts +3 -0
  78. package/src/runtime/app/paths/public.d.ts +1 -29
  79. package/src/runtime/app/paths/server.js +36 -17
  80. package/src/runtime/app/paths/types.d.ts +11 -19
  81. package/src/runtime/app/server/index.js +2 -2
  82. package/src/runtime/app/server/public.d.ts +201 -0
  83. package/src/runtime/app/server/remote/command.js +13 -11
  84. package/src/runtime/app/server/remote/form.js +61 -39
  85. package/src/runtime/app/server/remote/prerender.js +45 -45
  86. package/src/runtime/app/server/remote/query.js +106 -108
  87. package/src/runtime/app/server/remote/requested.js +27 -19
  88. package/src/runtime/app/server/remote/shared.js +76 -76
  89. package/src/runtime/app/service-worker/index.js +24 -0
  90. package/src/runtime/app/state/client.js +4 -2
  91. package/src/runtime/app/state/index.js +7 -5
  92. package/src/runtime/app/state/public.d.ts +72 -0
  93. package/src/runtime/app/state/server.js +3 -0
  94. package/src/runtime/app/stores.js +15 -78
  95. package/src/runtime/client/bundle.js +1 -1
  96. package/src/runtime/client/client-entry.js +3 -0
  97. package/src/runtime/client/client.js +1336 -648
  98. package/src/runtime/client/constants.js +3 -6
  99. package/src/runtime/client/entry.js +24 -3
  100. package/src/runtime/client/fetcher.js +25 -25
  101. package/src/runtime/client/ndjson.js +1 -1
  102. package/src/runtime/client/parse.js +1 -1
  103. package/src/runtime/client/payload.js +17 -0
  104. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  105. package/src/runtime/client/remote-functions/command.svelte.js +37 -61
  106. package/src/runtime/client/remote-functions/form.svelte.js +267 -178
  107. package/src/runtime/client/remote-functions/prerender.svelte.js +32 -11
  108. package/src/runtime/client/remote-functions/query/index.js +10 -17
  109. package/src/runtime/client/remote-functions/query/instance.svelte.js +64 -18
  110. package/src/runtime/client/remote-functions/query/proxy.js +5 -5
  111. package/src/runtime/client/remote-functions/query-batch.svelte.js +61 -70
  112. package/src/runtime/client/remote-functions/query-live/index.js +1 -1
  113. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +63 -21
  114. package/src/runtime/client/remote-functions/query-live/iterator.js +15 -12
  115. package/src/runtime/client/remote-functions/query-live/proxy.js +2 -12
  116. package/src/runtime/client/remote-functions/shared.svelte.js +90 -65
  117. package/src/runtime/client/snapshots.js +147 -0
  118. package/src/runtime/client/state.svelte.js +94 -55
  119. package/src/runtime/client/stream.js +3 -2
  120. package/src/runtime/client/types.d.ts +13 -9
  121. package/src/runtime/client/utils.js +22 -110
  122. package/src/runtime/components/root.svelte +56 -0
  123. package/src/runtime/env/dynamic/private.js +7 -0
  124. package/src/runtime/env/dynamic/public.js +7 -0
  125. package/src/runtime/env/static/private.js +6 -0
  126. package/src/runtime/env/static/public.js +6 -0
  127. package/src/runtime/form-utils.js +172 -61
  128. package/src/runtime/pathname.js +20 -32
  129. package/src/runtime/props.svelte.js +72 -0
  130. package/src/runtime/server/constants.js +0 -3
  131. package/src/runtime/server/cookie.js +72 -55
  132. package/src/runtime/server/csrf.js +65 -0
  133. package/src/runtime/server/data/index.js +49 -53
  134. package/src/runtime/server/dev.js +22 -0
  135. package/src/runtime/server/endpoint.js +6 -7
  136. package/src/runtime/server/env_module.js +0 -5
  137. package/src/runtime/server/errors.js +160 -0
  138. package/src/runtime/server/fetch.js +32 -39
  139. package/src/runtime/server/index.js +90 -64
  140. package/src/runtime/server/internal.js +71 -0
  141. package/src/runtime/server/page/actions.js +85 -64
  142. package/src/runtime/server/page/crypto.js +2 -2
  143. package/src/runtime/server/page/csp.js +88 -104
  144. package/src/runtime/server/page/data_serializer.js +24 -25
  145. package/src/runtime/server/page/index.js +36 -53
  146. package/src/runtime/server/page/load_data.js +50 -57
  147. package/src/runtime/server/page/render.js +173 -239
  148. package/src/runtime/server/page/respond_with_error.js +17 -31
  149. package/src/runtime/server/page/serialize_data.js +2 -13
  150. package/src/runtime/server/page/server_routing.js +85 -26
  151. package/src/runtime/server/remote-functions.js +640 -0
  152. package/src/runtime/server/respond.js +190 -131
  153. package/src/runtime/server/sourcemaps.js +183 -0
  154. package/src/runtime/server/state.js +53 -0
  155. package/src/runtime/server/utils.js +13 -155
  156. package/src/runtime/shared.js +20 -40
  157. package/src/runtime/utils.js +3 -0
  158. package/src/types/ambient-private.d.ts +11 -1
  159. package/src/types/ambient.d.ts +87 -36
  160. package/src/types/global-private.d.ts +25 -25
  161. package/src/types/internal.d.ts +163 -157
  162. package/src/types/private.d.ts +41 -1
  163. package/src/utils/error.js +28 -4
  164. package/src/utils/escape.js +9 -25
  165. package/src/utils/features.js +1 -1
  166. package/src/utils/filesystem.js +1 -23
  167. package/src/utils/fork.js +7 -2
  168. package/src/utils/hash.js +21 -0
  169. package/src/utils/http.js +8 -7
  170. package/src/utils/import.js +9 -2
  171. package/src/utils/imports.js +83 -0
  172. package/src/utils/mime.js +9 -0
  173. package/src/utils/page_nodes.js +6 -7
  174. package/src/utils/params.js +67 -0
  175. package/src/utils/regex.js +9 -0
  176. package/src/utils/routing.js +145 -73
  177. package/src/utils/shared-iterator.js +5 -0
  178. package/src/utils/streaming.js +14 -4
  179. package/src/utils/url.js +33 -2
  180. package/src/version.js +1 -1
  181. package/types/index.d.ts +2616 -2433
  182. package/types/index.d.ts.map +120 -106
  183. package/src/core/sync/write_ambient.js +0 -18
  184. package/src/core/sync/write_root.js +0 -148
  185. package/src/core/sync/write_tsconfig.js +0 -250
  186. package/src/exports/internal/server.js +0 -22
  187. package/src/exports/node/polyfills.js +0 -30
  188. package/src/runtime/server/app.js +0 -9
  189. package/src/runtime/server/remote.js +0 -457
  190. package/src/runtime/shared-server.js +0 -7
  191. package/src/runtime/telemetry/otel.js +0 -21
  192. package/src/runtime/telemetry/record_span.js +0 -65
  193. package/src/types/synthetic/$lib.md +0 -5
  194. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
  195. /package/src/{runtime/telemetry/noop.js → telemetry.js} +0 -0
@@ -1,22 +1,17 @@
1
- /** @import { ServerNodesResponse, ServerRedirectNode } from 'types' */
1
+ /** @import { LoadEvent, NavigationEvent } from '@sveltejs/kit' */
2
+ /** @import { ActionResult } from '$app/forms' */
3
+ /** @import { AfterNavigate, BeforeNavigate, GotoOptions, Navigation, NavigationTarget, NavigationType, OnNavigate } from '$app/navigation' */
4
+ /** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */
5
+ /** @import { NavigationFinished, NavigationIntent } from './types.js' */
2
6
  /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */
3
7
  /** @import { Query } from './remote-functions/query/instance.svelte.js' */
4
8
  /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */
5
9
  import { BROWSER, DEV } from 'esm-env';
6
- import * as svelte from 'svelte';
7
- import { HttpError, Redirect, SvelteKitError } from '@sveltejs/kit/internal';
8
- const { onMount, tick } = svelte;
9
- // Svelte 4 and under don't have `untrack`, so we have to fallback if `untrack` is not exported
10
- const untrack = svelte.untrack ?? ((value) => value());
11
- import {
12
- decode_params,
13
- decode_pathname,
14
- strip_hash,
15
- make_trackable,
16
- normalize_path
17
- } from '../../utils/url.js';
10
+ import { settled, tick, fork, onMount, hydrate, mount } from 'svelte';
11
+ import { HttpError, Redirect, SvelteKitError, HandledHttpError } from '@sveltejs/kit/internal';
12
+ import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js';
18
13
  import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
19
- import { parse, parse_server_route } from './parse.js';
14
+ import { parse_routes, parse_server_route } from './parse.js';
20
15
  import * as storage from './session-storage.js';
21
16
  import {
22
17
  find_anchor,
@@ -26,24 +21,27 @@ import {
26
21
  is_external_url,
27
22
  origin,
28
23
  scroll_state,
29
- notifiable_store,
30
- create_updated_store,
31
24
  load_css
32
25
  } from './utils.js';
33
- import { base } from '$app/paths';
26
+ import { base, set_match_implementation } from '$app/paths/internal/client';
34
27
  import * as devalue from 'devalue';
35
28
  import {
36
- HISTORY_INDEX,
37
- NAVIGATION_INDEX,
29
+ HISTORY_INFO_KEY,
30
+ HISTORY_METADATA_KEY,
38
31
  PRELOAD_PRIORITIES,
39
- SCROLL_KEY,
40
- STATES_KEY,
41
- SNAPSHOT_KEY,
42
- PAGE_URL_KEY
32
+ SNAPSHOT_KEY
43
33
  } from './constants.js';
34
+ import {
35
+ capture_navigation_snapshot,
36
+ current_registrations,
37
+ delete_navigation_snapshot,
38
+ init_snapshots,
39
+ persist_navigation_snapshots,
40
+ restore_navigation_snapshot
41
+ } from './snapshots.js';
44
42
  import { validate_page_exports } from '../../utils/exports.js';
43
+ import { add_deprecated_handle_error_properties } from '../../utils/error.js';
45
44
  import { noop } from '../../utils/functions.js';
46
- import { compact } from '../../utils/array.js';
47
45
  import {
48
46
  INVALIDATED_PARAM,
49
47
  TRAILING_SLASH_PARAM,
@@ -51,42 +49,68 @@ import {
51
49
  validate_depends,
52
50
  validate_load_response
53
51
  } from '../shared.js';
54
- import { get_message, get_status } from '../../utils/error.js';
55
- import { writable } from 'svelte/store';
56
- import { page, update, navigating } from './state.svelte.js';
57
- import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
58
- import { noop_span } from '../telemetry/noop.js';
52
+
53
+ import { page, navigating, updated, notify_version } from './state.svelte.js';
54
+ import { payload } from './payload.js';
55
+ import {
56
+ add_data_suffix,
57
+ add_resolution_suffix,
58
+ route_id_resolution_pathname
59
+ } from '../pathname.js';
60
+ import { noop_span } from '../../telemetry.js';
59
61
  import { read_ndjson } from './ndjson.js';
62
+ import Root from '../components/root.svelte';
63
+ import { Props, RenderNode } from '../props.svelte.js';
64
+ import { init_transport, parse, stringify } from '#app/internal/transport';
65
+
66
+ /**
67
+ * @typedef {{
68
+ * historyIndex: number;
69
+ * navigationIndex: number;
70
+ * pageUrl?: string;
71
+ * state: string;
72
+ * persistState: boolean;
73
+ * resetIndex: number;
74
+ * }} HistoryMetadata
75
+ */
60
76
 
61
77
  export { load_css };
62
78
  const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
63
79
 
64
80
  let errored = false;
81
+
65
82
  /**
66
- * Set via transformError, reset and read at the end of navigate.
67
- * Necessary because a navigation might succeed loading but during rendering
68
- * an error occurs, at which point the navigation result needs to be overridden with the error result.
69
- * TODO this is all very hacky, rethink for SvelteKit 3 where we can assume Svelte 5 and do an overhaul of client.js
70
- * @type {{ error: App.Error, status: number } | null}
83
+ * `reset` functions for `<svelte:boundary>`s in the generated root that have
84
+ * failed. A failed boundary stays failed until `reset()` is called — prop
85
+ * updates alone don't re-render its content so without resetting, a client
86
+ * navigation away from a render error would leave the stale `+error.svelte`
87
+ * mounted. The boundary's `onerror` populates this array; `navigate` drains it
88
+ * after applying the new props. See sveltejs/kit#15694.
89
+ * @type {Set<() => void>}
71
90
  */
72
- let rendering_error = null;
91
+ const resetters = new Set();
73
92
 
74
- // We track the scroll position associated with each history entry in sessionStorage,
93
+ // We track information associated with each history entry in sessionStorage,
75
94
  // rather than on history.state itself, because when navigation is driven by
76
- // popstate it's too late to update the scroll position associated with the
95
+ // popstate it's too late to access the options or update the focus position associated with the
77
96
  // state we're navigating from
78
97
  /**
79
- * history index -> { x, y }
80
- * @type {Record<number, { x: number; y: number }>}
98
+ * @type {Record<number, { scroll?: { x: number; y: number }; resetIndex?: number }>}
81
99
  */
82
- const scroll_positions = storage.get(SCROLL_KEY) ?? {};
100
+ const history_info = storage.get(HISTORY_INFO_KEY) ?? {};
83
101
 
84
102
  /**
85
103
  * navigation index -> any
86
104
  * @type {Record<string, any[]>}
105
+ * @deprecated TODO 4.0 get rid of this
87
106
  */
88
107
  const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
89
108
 
109
+ init_snapshots(() => (started && !is_navigating && !updating ? current_history_index : null));
110
+
111
+ /** @type {Props} */
112
+ let props;
113
+
90
114
  if (DEV && BROWSER) {
91
115
  let warned = false;
92
116
 
@@ -100,14 +124,19 @@ if (DEV && BROWSER) {
100
124
  let stack = new Error().stack?.split('\n');
101
125
  if (!stack) return;
102
126
  if (!stack[0].includes('https:') && !stack[0].includes('http:')) stack = stack.slice(1); // Chrome includes the error message in the stack
103
- stack = stack.slice(2); // remove `warn` and the place where `warn` was called
104
- // Can be falsy if was called directly from an anonymous function
105
- if (stack[0]?.includes(current_module_url)) return;
127
+
128
+ // skip over `warn` and the place where `warn` was called
129
+ const frame = stack[2];
130
+
131
+ // Ignore calls that happen inside dependencies, including SvelteKit.
132
+ // 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)
133
+ // `frame` can be falsy if we came from an anonymous function
134
+ if (frame?.includes('node_modules') || frame?.includes(current_module_url)) return;
106
135
 
107
136
  warned = true;
108
137
 
109
138
  console.warn(
110
- "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."
139
+ "Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use `goto(...)` from `$app/navigation` instead."
111
140
  );
112
141
  };
113
142
 
@@ -124,18 +153,61 @@ if (DEV && BROWSER) {
124
153
  };
125
154
  }
126
155
 
127
- export const stores = {
128
- url: /* @__PURE__ */ notifiable_store({}),
129
- page: /* @__PURE__ */ notifiable_store({}),
130
- navigating: /* @__PURE__ */ writable(
131
- /** @type {import('@sveltejs/kit').Navigation | null} */ (null)
132
- ),
133
- updated: /* @__PURE__ */ create_updated_store()
134
- };
135
-
136
156
  /** @param {number} index */
137
- function update_scroll_positions(index) {
138
- scroll_positions[index] = scroll_state();
157
+ function capture_scroll(index) {
158
+ history_info[index].scroll = scroll_state();
159
+ }
160
+
161
+ /**
162
+ * @param {number} index
163
+ * @param {Pick<HistoryMetadata, 'resetIndex'>} options
164
+ */
165
+ function set_history_options(index, options) {
166
+ history_info[index] = {
167
+ ...history_info[index],
168
+ resetIndex: options.resetIndex
169
+ };
170
+ }
171
+
172
+ /** @param {boolean} reset */
173
+ function blur_active_element(reset) {
174
+ if (
175
+ reset &&
176
+ document.activeElement instanceof HTMLElement &&
177
+ document.activeElement !== document.body
178
+ ) {
179
+ document.activeElement.blur();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * @param {URL} url
185
+ * @param {{ x: number; y: number } | null | undefined} scroll
186
+ * @param {boolean} reset
187
+ * @param {Element | null} active_element
188
+ */
189
+ function reset_scroll_and_focus(url, scroll, reset, active_element) {
190
+ /** @type {Element | null} */
191
+ let deep_linked = null;
192
+
193
+ if (autoscroll) {
194
+ if (scroll) {
195
+ scrollTo(scroll.x, scroll.y);
196
+ } else if ((deep_linked = get_hash_element(url))) {
197
+ deep_linked.scrollIntoView();
198
+ } else {
199
+ scrollTo(0, 0);
200
+ }
201
+ }
202
+
203
+ const changed_focus =
204
+ document.activeElement !== active_element && document.activeElement !== document.body;
205
+
206
+ if (reset && !changed_focus) {
207
+ reset_focus(url, !deep_linked);
208
+ }
209
+
210
+ autoscroll = true;
139
211
  }
140
212
 
141
213
  /**
@@ -145,9 +217,12 @@ function update_scroll_positions(index) {
145
217
  function clear_onward_history(current_history_index, current_navigation_index) {
146
218
  // if we navigated back, then pushed a new state, we can
147
219
  // release memory by pruning the scroll/snapshot lookup
220
+ // the new entry reuses the first abandoned index, so its slot must start clean
221
+ delete_navigation_snapshot(current_history_index);
148
222
  let i = current_history_index + 1;
149
- while (scroll_positions[i]) {
150
- delete scroll_positions[i];
223
+ while (history_info[i]) {
224
+ delete history_info[i];
225
+ delete_navigation_snapshot(i);
151
226
  i += 1;
152
227
  }
153
228
 
@@ -163,7 +238,8 @@ function clear_onward_history(current_history_index, current_navigation_index) {
163
238
  * Returns a `Promise` that never resolves (to prevent any
164
239
  * subsequent work, e.g. history manipulation, from happening)
165
240
  * @param {URL} url
166
- * @param {boolean} [replace] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
241
+ * @param {boolean} [replace] if `true`, will replace the current `history` entry rather than creating a new one with `pushState`
242
+ * @returns {Promise<any>} a promise that never resolves
167
243
  */
168
244
  function native_navigation(url, replace = false) {
169
245
  if (replace) {
@@ -187,7 +263,7 @@ async function update_service_worker() {
187
263
  }
188
264
  }
189
265
 
190
- /** @type {import('types').CSRRoute[]} All routes of the app. Only available when kit.router.resolution=client */
266
+ /** @type {import('types').CSRRoute[]} All routes of the app. Only available when router.resolution=client */
191
267
  let routes;
192
268
  /** @type {import('types').CSRPageNodeLoader} */
193
269
  let default_layout_loader;
@@ -202,30 +278,24 @@ let target;
202
278
  export let app;
203
279
 
204
280
  /**
205
- * Data that was serialized during SSR for queries/forms/commands.
206
- * This is cleared before client-side loads run.
207
- * @type {Record<string, any>}
281
+ * Data that was serialized during SSR for queries/forms/commands, stored as
282
+ * `{ v }` (value) or `{ e }` (error) nodes so that failed states survive hydration.
283
+ * Entries are deleted as they are consumed (when the corresponding resource is created).
284
+ * @type {Record<string, RemoteFunctionDataNode>}
208
285
  */
209
- export let query_responses = {};
286
+ export const query_responses = {};
210
287
 
211
288
  /**
212
- * Data that was serialized during SSR for prerender functions.
289
+ * Data that was serialized during SSR for prerender functions, stored as
290
+ * `{ v }` (value) or `{ e }` (error) nodes.
213
291
  * This persists across client-side navigations.
214
- * @type {Record<string, any>}
292
+ * @type {Record<string, RemoteFunctionDataNode>}
215
293
  */
216
- export let prerender_responses = {};
294
+ export const prerender_responses = {};
217
295
 
218
296
  /** @type {Array<((url: URL) => boolean)>} */
219
297
  const invalidated = [];
220
298
 
221
- /**
222
- * An array of the `+layout.svelte` and `+page.svelte` component instances
223
- * that currently live on the page — used for capturing and restoring snapshots.
224
- * It's updated/manipulated through `bind:this` in `Root.svelte`.
225
- * @type {import('svelte').SvelteComponent[]}
226
- */
227
- const components = [];
228
-
229
299
  /** @type {{id: string, token: {}, promise: Promise<import('./types.js').NavigationResult>, fork: Promise<import('svelte').Fork | null> | null} | null} */
230
300
  let load_cache = null;
231
301
 
@@ -246,20 +316,49 @@ function discard_load_cache() {
246
316
  */
247
317
  const reroute_cache = new Map();
248
318
 
319
+ /**
320
+ * Sentinel for a route that exists but has no code to preload, i.e. a `+server.js` with no
321
+ * `+page`. Cached like a parsed route so that repeated `preloadCode(id)` calls for the same
322
+ * id don't re-request it.
323
+ */
324
+ const ENDPOINT_ONLY = Symbol('endpoint only');
325
+
326
+ /**
327
+ * Cache of route ID -> parsed route for server-side route resolution.
328
+ * Populated whenever a server route resolution occurs (`match`, link preloading,
329
+ * hydration), so that `preloadCode(id)` doesn't need an extra server round trip.
330
+ * Lives until full page reload.
331
+ * @type {Map<string, import('types').CSRRoute | typeof ENDPOINT_ONLY>}
332
+ */
333
+ const route_id_cache = new Map();
334
+
335
+ /**
336
+ * Parse a server-provided route and record it in `route_id_cache`, so that a subsequent
337
+ * `preloadCode(id)` for the same route doesn't need another server round trip. Always use
338
+ * this rather than calling `parse_server_route` directly.
339
+ * @param {import('types').CSRRouteServer} server_route
340
+ * @returns {import('types').CSRRoute}
341
+ */
342
+ function parse_and_cache_server_route(server_route) {
343
+ const route = parse_server_route(server_route, app.nodes);
344
+ route_id_cache.set(route.id, route);
345
+ return route;
346
+ }
347
+
249
348
  /**
250
349
  * Note on before_navigate_callbacks, on_navigate_callbacks and after_navigate_callbacks:
251
350
  * do not re-assign as some closures keep references to these Sets
252
351
  */
253
- /** @type {Set<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */
352
+ /** @type {Set<(navigation: BeforeNavigate) => void>} */
254
353
  const before_navigate_callbacks = new Set();
255
354
 
256
- /** @type {Set<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
355
+ /** @type {Set<(navigation: OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
257
356
  const on_navigate_callbacks = new Set();
258
357
 
259
- /** @type {Set<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */
358
+ /** @type {Set<(navigation: AfterNavigate) => void>} */
260
359
  const after_navigate_callbacks = new Set();
261
360
 
262
- /** @type {import('./types.js').NavigationState & { nav: import('@sveltejs/kit').NavigationEvent }} */
361
+ /** @type {import('./types.js').NavigationState & { nav: NavigationEvent }} */
263
362
  let current = {
264
363
  branch: [],
265
364
  error: null,
@@ -275,23 +374,41 @@ let started = false;
275
374
  let autoscroll = true;
276
375
  let updating = false;
277
376
  let is_navigating = false;
278
- let hash_navigating = false;
377
+ /** @type {HistoryMetadata | null} */
378
+ let hash_navigating = null;
279
379
  /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
280
380
  let has_navigated = false;
281
381
 
282
382
  let force_invalidation = false;
283
383
 
284
- /** @type {import('svelte').SvelteComponent} */
285
- let root;
286
-
287
384
  /** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
288
385
  let current_history_index;
289
386
 
290
387
  /** @type {number} */
291
388
  let current_navigation_index;
292
389
 
293
- /** @type {{}} */
294
- let token;
390
+ /** @type {number} */
391
+ let current_reset_index;
392
+
393
+ /**
394
+ * @param {any} [state]
395
+ * @returns {HistoryMetadata | undefined}
396
+ */
397
+ function get_history_metadata(state = history.state) {
398
+ return state?.[HISTORY_METADATA_KEY];
399
+ }
400
+
401
+ /** @type {{}} Token for the latest navigation. Updated on new navigations */
402
+ let navigation_token;
403
+
404
+ /**
405
+ * @type {{}}
406
+ * The latest invalidate(All) token. Superseeded by both later invalidate(All)s and navigations.
407
+ * This is separate to navigation_token because an invalidate(All) might be triggered while a navigation
408
+ * is in progress, and we want to be able to finish this navigation (unless the invalidation finishes before
409
+ * it and redirects, in which case we will do the redirect triggered by the invalidation).
410
+ */
411
+ let invalidation_token;
295
412
 
296
413
  /**
297
414
  * A set of tokens which are associated to current preloads.
@@ -299,10 +416,11 @@ let token;
299
416
  * If a preload token is in the set and the preload errors, the error
300
417
  * handling logic (for example reloading) is skipped.
301
418
  */
419
+ /** @type {Set<{}>} */
302
420
  const preload_tokens = new Set();
303
421
 
304
422
  /** @type {Promise<void> | null} */
305
- export let pending_invalidate;
423
+ let pending_invalidate;
306
424
 
307
425
  /**
308
426
  * @type {Map<string, Map<string, CacheEntry<Query<any>>>>}
@@ -316,21 +434,65 @@ export const query_map = new Map();
316
434
  */
317
435
  export const live_query_map = new Map();
318
436
 
437
+ set_match_implementation(async (url) => {
438
+ if (typeof url === 'string') {
439
+ url = new URL(url, location.href);
440
+ }
441
+
442
+ const intent = await get_navigation_intent(url, false);
443
+
444
+ if (intent) {
445
+ return {
446
+ id: /** @type {import('$app/types').RouteId} */ (intent.route.id),
447
+ params: intent.params
448
+ };
449
+ }
450
+
451
+ return null;
452
+ });
453
+
454
+ let embedded_start = Promise.resolve();
455
+
456
+ /**
457
+ * TODO this indirection is a grotesque workaround for the fact that multiple apps
458
+ * can operate on the same shared mutable state. This is fundamentally unsound,
459
+ * and the `start`/`_start` distinction does not fix it, but it does get the
460
+ * tests passing. We need to rethink this whole thing but not right now
461
+ * @param {import('./types.js').SvelteKitApp} _app
462
+ * @param {HTMLElement} _target
463
+ * @param {Parameters<typeof _hydrate>[1]} [data]
464
+ */
465
+ export function start(_app, _target, data) {
466
+ if (__SVELTEKIT_EMBEDDED__) {
467
+ const start_promise = embedded_start.then(() => _start(_app, _target, data));
468
+ embedded_start = start_promise.catch(noop);
469
+ return start_promise;
470
+ }
471
+
472
+ return _start(_app, _target, data);
473
+ }
474
+
319
475
  /**
320
476
  * @param {import('./types.js').SvelteKitApp} _app
321
477
  * @param {HTMLElement} _target
322
- * @param {Parameters<typeof _hydrate>[1]} [hydrate]
478
+ * @param {Parameters<typeof _hydrate>[1]} [data]
323
479
  */
324
- export async function start(_app, _target, hydrate) {
480
+ async function _start(_app, _target, data) {
325
481
  if (DEV && _target === document.body) {
326
482
  console.warn(
327
483
  '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>'
328
484
  );
329
485
  }
330
486
 
331
- if (__SVELTEKIT_PAYLOAD__) {
332
- query_responses = __SVELTEKIT_PAYLOAD__.query ?? {};
333
- prerender_responses = __SVELTEKIT_PAYLOAD__.prerender ?? {};
487
+ if (payload.data) {
488
+ const { q = {}, p = {}, l = {}, f = {} } = payload.data;
489
+
490
+ // store the whole nodes — error records seed the corresponding
491
+ // resources in a failed state when they are created during hydration
492
+ for (const k in q) query_responses[k] = q[k];
493
+ for (const k in l) query_responses[k] = l[k];
494
+ for (const k in f) query_responses[k] = f[k];
495
+ for (const k in p) prerender_responses[k] = p[k];
334
496
  }
335
497
 
336
498
  // detect basic auth credentials in the current URL
@@ -343,9 +505,11 @@ export async function start(_app, _target, hydrate) {
343
505
 
344
506
  app = _app;
345
507
 
508
+ init_transport(app.hooks.transport ?? {});
509
+
346
510
  await _app.hooks.init?.();
347
511
 
348
- routes = __SVELTEKIT_CLIENT_ROUTING__ ? parse(_app) : [];
512
+ routes = __SVELTEKIT_CLIENT_ROUTING__ ? parse_routes(_app) : [];
349
513
  container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
350
514
  target = _target;
351
515
 
@@ -353,31 +517,56 @@ export async function start(_app, _target, hydrate) {
353
517
  // connectivity errors after initialisation don't nuke the app
354
518
  default_layout_loader = _app.nodes[0];
355
519
  default_error_loader = _app.nodes[1];
356
- void default_layout_loader();
357
- void default_error_loader();
358
520
 
359
- current_history_index = history.state?.[HISTORY_INDEX];
360
- current_navigation_index = history.state?.[NAVIGATION_INDEX];
521
+ const [root_layout, root_error] = await Promise.all([
522
+ default_layout_loader(),
523
+ default_error_loader()
524
+ ]);
525
+
526
+ const tree = new RenderNode(root_layout.component, root_error.component);
527
+
528
+ props = new Props({
529
+ page,
530
+ tree,
531
+ form: undefined,
532
+ error: undefined,
533
+ onerror: (_, reset) => resetters.add(reset)
534
+ });
535
+
536
+ const history_metadata = get_history_metadata();
537
+ current_history_index = history_metadata?.historyIndex ?? 0;
538
+ current_navigation_index = history_metadata?.navigationIndex ?? 0;
539
+ current_reset_index = history_metadata?.resetIndex ?? 0;
361
540
 
362
541
  if (!current_history_index) {
363
542
  // we use Date.now() as an offset so that cross-document navigations
364
543
  // within the app don't result in data loss
365
- current_history_index = current_navigation_index = Date.now();
544
+ current_history_index = current_navigation_index = current_reset_index = Date.now();
366
545
 
367
546
  // create initial history entry, so we can return here
368
547
  history.replaceState(
369
548
  {
370
549
  ...history.state,
371
- [HISTORY_INDEX]: current_history_index,
372
- [NAVIGATION_INDEX]: current_navigation_index
550
+ [HISTORY_METADATA_KEY]: {
551
+ historyIndex: current_history_index,
552
+ navigationIndex: current_navigation_index,
553
+ state: stringify({}),
554
+ persistState: false,
555
+ resetIndex: current_history_index
556
+ }
373
557
  },
374
558
  ''
375
559
  );
376
560
  }
377
561
 
562
+ set_history_options(
563
+ current_history_index,
564
+ /** @type {HistoryMetadata} */ (get_history_metadata())
565
+ );
566
+
378
567
  // if we reload the page, or Cmd-Shift-T back to it,
379
568
  // recover scroll position
380
- const scroll = scroll_positions[current_history_index];
569
+ const scroll = history_info[current_history_index]?.scroll;
381
570
  function restore_scroll() {
382
571
  if (scroll) {
383
572
  history.scrollRestoration = 'manual';
@@ -385,15 +574,17 @@ export async function start(_app, _target, hydrate) {
385
574
  }
386
575
  }
387
576
 
388
- if (hydrate) {
577
+ if (data) {
389
578
  restore_scroll();
390
579
 
391
- await _hydrate(target, hydrate);
580
+ await _hydrate(target, data);
392
581
  } else {
393
582
  await navigate({
394
583
  type: 'enter',
395
584
  url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href),
396
- replace_state: true
585
+ replace_state: true,
586
+ state: history_metadata?.persistState ? parse(history_metadata.state) : {},
587
+ persist_state: history_metadata?.persistState ?? false
397
588
  });
398
589
 
399
590
  restore_scroll();
@@ -402,15 +593,17 @@ export async function start(_app, _target, hydrate) {
402
593
  _start_router();
403
594
  }
404
595
 
405
- async function _invalidate(include_load_functions = true, reset_page_state = true) {
596
+ async function _invalidate(reset_page_state = true) {
406
597
  // Accept all invalidations as they come, don't swallow any while another invalidation
407
598
  // is running because subsequent invalidations may make earlier ones outdated,
408
599
  // but batch multiple synchronous invalidations.
409
600
  await (pending_invalidate ||= Promise.resolve());
410
- if (!pending_invalidate) return;
601
+ if (pending_invalidate === null) return;
411
602
  pending_invalidate = null;
412
603
 
413
- const nav_token = (token = {});
604
+ const token = (invalidation_token = {});
605
+ const nav_token = navigation_token;
606
+ const navigating = is_navigating;
414
607
  const intent = await get_navigation_intent(current.url, true);
415
608
 
416
609
  // Clear preload, it might be affected by the invalidation.
@@ -439,31 +632,36 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
439
632
  }
440
633
  }
441
634
 
442
- if (include_load_functions) {
443
- const prev_state = page.state;
444
- const navigation_result = intent && (await load_route(intent));
445
- if (!navigation_result || nav_token !== token) return;
635
+ const prev_state = page.state;
636
+ const prev_shallow = page.shallow;
637
+ const navigation_result = intent && (await load_route(intent));
638
+ if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
639
+ return;
640
+ }
641
+
642
+ if (navigation_result.type === 'redirect') {
643
+ return _goto(
644
+ new URL(navigation_result.location, current.url).href,
645
+ { replace: true },
646
+ 1,
647
+ token
648
+ );
649
+ }
446
650
 
447
- if (navigation_result.type === 'redirect') {
448
- return _goto(
449
- new URL(navigation_result.location, current.url).href,
450
- { replaceState: true },
451
- 1,
452
- nav_token
453
- );
454
- }
651
+ // A navigation started before the invalidation and ended before it finished. The invalidation did not redirect,
652
+ // hence it likely contains outdated data now, so we ignore it.
653
+ if (navigating && !is_navigating) {
654
+ return;
655
+ }
455
656
 
456
- // This is a bit hacky but allows us not having to pass that boolean around, making things harder to reason about
457
- if (!reset_page_state) {
458
- navigation_result.props.page.state = prev_state;
459
- }
460
- update(navigation_result.props.page);
461
- current = { ...navigation_result.state, nav: current.nav };
462
- reset_invalidation();
463
- root.$set(navigation_result.props);
464
- } else {
465
- reset_invalidation();
657
+ // Preserve `page.state` when invalidating without resetting it (e.g. `refresh`/`refreshAll`)
658
+ if (!reset_page_state) {
659
+ navigation_result.props.page.state = prev_state;
466
660
  }
661
+ navigation_result.props.page.shallow = prev_shallow;
662
+ apply_navigation_result(navigation_result);
663
+ current = { ...navigation_result.state, nav: current.nav };
664
+ reset_invalidation();
467
665
 
468
666
  // only wait for promises that are connected to queries that still exist
469
667
  /** @type {Promise<any>[]} */
@@ -492,61 +690,86 @@ function reset_invalidation() {
492
690
  force_invalidation = false;
493
691
  }
494
692
 
495
- /** @param {number} index */
693
+ let warned_snapshot_export = false;
694
+
695
+ /**
696
+ * @param {number} index
697
+ * @deprecated TODO 4.0 get rid of this
698
+ */
496
699
  function capture_snapshot(index) {
497
- if (components.some((c) => c?.snapshot)) {
498
- snapshots[index] = components.map((c) => c?.snapshot?.capture());
700
+ if (props.components.some((c) => c?.snapshot)) {
701
+ if (DEV && !warned_snapshot_export) {
702
+ warned_snapshot_export = true;
703
+ console.warn(
704
+ '`export const snapshot` is deprecated. Use the `snapshot` helper from `$app/navigation` instead.'
705
+ );
706
+ }
707
+ snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
499
708
  }
500
709
  }
501
710
 
502
- /** @param {number} index */
711
+ /**
712
+ * @param {number} index
713
+ * @deprecated TODO 4.0 get rid of this
714
+ */
503
715
  function restore_snapshot(index) {
504
716
  snapshots[index]?.forEach((value, i) => {
505
- components[i]?.snapshot?.restore(value);
717
+ props.components[i]?.snapshot?.restore(value);
506
718
  });
507
719
  }
508
720
 
509
721
  function persist_state() {
510
- update_scroll_positions(current_history_index);
511
- storage.set(SCROLL_KEY, scroll_positions);
722
+ capture_scroll(current_history_index);
723
+ storage.set(HISTORY_INFO_KEY, history_info);
512
724
 
513
725
  capture_snapshot(current_navigation_index);
514
726
  storage.set(SNAPSHOT_KEY, snapshots);
727
+
728
+ persist_navigation_snapshots(current_history_index);
515
729
  }
516
730
 
517
731
  /**
518
732
  * @param {string | URL} url
519
- * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
520
- * @param {number} redirect_count
733
+ * @param {{ type?: NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event; action_result?: ActionResult }} [options]
734
+ * @param {number} [redirect_count]
521
735
  * @param {{}} [nav_token]
736
+ * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
737
+ * @returns {Promise<void>}
522
738
  */
523
- export async function _goto(url, options, redirect_count, nav_token) {
739
+ export async function _goto(url, options = {}, redirect_count = 0, nav_token = {}, intent) {
524
740
  /** @type {Set<string>} */
525
741
  let query_keys;
526
742
  /** @type {Set<string>} */
527
743
  let live_query_keys;
528
744
 
529
- // Clear preload cache when invalidateAll is true to ensure fresh data
745
+ // Clear preload cache when refreshAll is true to ensure fresh data
530
746
  // after form submissions or explicit invalidations
531
- if (options.invalidateAll) {
747
+ if (options.refreshAll) {
532
748
  discard_load_cache();
533
749
  }
534
750
 
535
751
  await navigate({
536
- type: 'goto',
752
+ type: options.type ?? 'goto',
537
753
  url: resolve_url(url),
538
- keepfocus: options.keepFocus,
539
- noscroll: options.noScroll,
540
- replace_state: options.replaceState,
754
+ reset: options.reset,
755
+ replace_state: options.replace,
541
756
  state: options.state,
757
+ persist_state: options.persistState,
758
+ event: options.event,
759
+ action_result: options.action_result,
542
760
  redirect_count,
543
761
  nav_token,
762
+ intent,
544
763
  accept: () => {
545
- if (options.invalidateAll) {
764
+ if (options.refreshAll) {
546
765
  force_invalidation = true;
547
766
  query_keys = new Set();
548
767
  for (const [id, entries] of query_map) {
549
- for (const payload of entries.keys()) {
768
+ for (const [payload, entry] of entries) {
769
+ // don't refresh yet, as some queries will be unrendered,
770
+ // but clear caches so that newly rendered queries
771
+ // don't use stale data. TODO same for `live_query_map`
772
+ entry.resource?.reset();
550
773
  query_keys.add(create_remote_key(id, payload));
551
774
  }
552
775
  }
@@ -564,17 +787,16 @@ export async function _goto(url, options, redirect_count, nav_token) {
564
787
  }
565
788
  });
566
789
 
567
- if (options.invalidateAll) {
790
+ if (options.refreshAll) {
568
791
  // TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
569
792
  // when a query in a layout that still exists after page change is refreshed earlier than this
570
- void svelte
571
- .tick()
572
- .then(svelte.tick)
793
+ void tick()
794
+ .then(tick)
573
795
  .then(() => {
574
796
  for (const [id, entries] of query_map) {
575
797
  for (const [payload, { resource }] of entries) {
576
798
  if (query_keys?.has(create_remote_key(id, payload))) {
577
- void resource.refresh();
799
+ void resource.start();
578
800
  }
579
801
  }
580
802
  }
@@ -603,18 +825,15 @@ async function _preload_data(intent) {
603
825
  load_cache = {
604
826
  id: intent.id,
605
827
  token: preload,
606
- promise: load_route({ ...intent, preload }).then((result) => {
828
+ promise: load_route({ ...intent, preload }).finally(() => {
607
829
  preload_tokens.delete(preload);
608
- if (result.type === 'loaded' && result.state.error) {
609
- // Don't cache errors, because they might be transient
610
- discard_load_cache();
611
- }
612
- return result;
613
830
  }),
614
831
  fork: null
615
832
  };
616
833
 
617
- if (__SVELTEKIT_FORK_PRELOADS__ && svelte.fork) {
834
+ load_cache.promise.catch(discard_load_cache);
835
+
836
+ if (__SVELTEKIT_FORK_PRELOADS__) {
618
837
  const lc = load_cache;
619
838
 
620
839
  lc.fork = lc.promise.then((result) => {
@@ -622,9 +841,8 @@ async function _preload_data(intent) {
622
841
  // resolve, bail rather than creating an orphan fork
623
842
  if (lc === load_cache && result.type === 'loaded') {
624
843
  try {
625
- return svelte.fork(() => {
626
- root.$set(result.props);
627
- update(result.props.page);
844
+ return fork(() => {
845
+ apply_navigation_result(result);
628
846
  });
629
847
  } catch {
630
848
  // if it errors, it's because the experimental flag isn't enabled in Svelte
@@ -639,6 +857,53 @@ async function _preload_data(intent) {
639
857
  return load_cache.promise;
640
858
  }
641
859
 
860
+ /**
861
+ * Fetch and parse the route with the given ID from the server-side route resolution endpoint.
862
+ * Returns `ENDPOINT_ONLY` if the route exists but has no code to preload, or `undefined` if
863
+ * there is no such route. Only used when `router.resolution === 'server'`.
864
+ * @param {string} id
865
+ * @returns {Promise<import('types').CSRRoute | typeof ENDPOINT_ONLY | undefined>}
866
+ */
867
+ async function load_route_by_id(id) {
868
+ /** @type {{ route?: import('types').CSRRouteServer, endpoint_only?: boolean }} */
869
+ let module;
870
+
871
+ try {
872
+ module = await import(
873
+ /* @vite-ignore */
874
+ base + route_id_resolution_pathname(id)
875
+ );
876
+ } catch {
877
+ // if there's no module at that path the response is a 404 (or, on a static
878
+ // host, fallback HTML with the wrong MIME type) and the import rejects —
879
+ // treat it the same as an unknown route rather than surfacing a cryptic error
880
+ return;
881
+ }
882
+
883
+ if (module.endpoint_only) {
884
+ // The route exists, it just has no code to preload.
885
+ route_id_cache.set(id, ENDPOINT_ONLY);
886
+ return ENDPOINT_ONLY;
887
+ }
888
+
889
+ if (!module.route) return;
890
+
891
+ return parse_and_cache_server_route(module.route);
892
+ }
893
+
894
+ /**
895
+ * Import the modules for a route's layout and leaf nodes, without running `load` functions.
896
+ * @param {import('types').CSRRoute} route
897
+ * @returns {Promise<void>}
898
+ */
899
+ async function load_route_nodes(route) {
900
+ await Promise.all(
901
+ /** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
902
+ [...route.layouts, route.leaf].filter(Boolean)
903
+ ).map(([, node_loader]) => node_loader())
904
+ );
905
+ }
906
+
642
907
  /**
643
908
  * @param {URL} url
644
909
  * @returns {Promise<void>}
@@ -647,24 +912,20 @@ async function _preload_code(url) {
647
912
  const route = (await get_navigation_intent(url, false))?.route;
648
913
 
649
914
  if (route) {
650
- await Promise.all(
651
- /** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
652
- [...route.layouts, route.leaf].filter(Boolean)
653
- ).map((load) => load[1]())
654
- );
915
+ await load_route_nodes(route);
655
916
  }
656
917
  }
657
918
 
658
919
  /**
659
920
  * @param {import('./types.js').NavigationFinished} result
660
921
  * @param {HTMLElement} target
661
- * @param {boolean} hydrate
922
+ * @param {boolean} should_hydrate
662
923
  */
663
- async function initialize(result, target, hydrate) {
924
+ async function initialize(result, target, should_hydrate) {
664
925
  if (__SVELTEKIT_DEV__ && result.state.error && document.querySelector('vite-error-overlay'))
665
926
  return;
666
927
 
667
- /** @type {import('@sveltejs/kit').NavigationEvent} */
928
+ /** @type {NavigationEvent} */
668
929
  const nav = {
669
930
  params: current.params,
670
931
  route: { id: current.route?.id ?? null },
@@ -682,48 +943,46 @@ async function initialize(result, target, hydrate) {
682
943
  if (style) style.remove();
683
944
  }
684
945
 
685
- update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page));
946
+ apply_navigation_result(result);
947
+
948
+ // TODO treeshake `hydrate` in csr mode
949
+ const render = should_hydrate ? hydrate : mount;
686
950
 
687
- root = new app.root({
951
+ render(Root, {
688
952
  target,
689
- props: { ...result.props, stores, components },
690
- hydrate,
691
- // @ts-ignore Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync
692
- sync: false,
693
- // @ts-ignore Svelte 5 specific: transformError allows to transform errors before they are passed to boundaries
694
- transformError: __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__
695
- ? /** @param {unknown} e */ async (e) => {
696
- const error = await handle_error(e, current.nav);
697
- rendering_error = { error, status: get_status(e) };
698
- page.error = error;
699
- page.status = rendering_error.status;
700
- return error;
701
- }
702
- : undefined
953
+ props,
954
+ transformError: /** @param {unknown} e */ async (e) => {
955
+ const error = await handle_error(e, current.nav);
956
+ page.error = error;
957
+ page.status = error.status;
958
+ return error;
959
+ }
703
960
  });
704
961
 
705
962
  // Wait for a microtask in case svelte experimental async is enabled,
706
963
  // which causes component script blocks to run asynchronously
707
964
  void (await Promise.resolve());
708
965
 
709
- restore_snapshot(current_navigation_index);
710
-
711
- if (hydrate) {
712
- /** @type {import('@sveltejs/kit').AfterNavigate} */
966
+ if (should_hydrate) {
967
+ /** @type {AfterNavigate} */
713
968
  const navigation = {
714
969
  from: null,
715
970
  to: {
716
971
  ...nav,
717
- scroll: scroll_positions[current_history_index] ?? scroll_state()
972
+ scroll: history_info[current_history_index]?.scroll ?? scroll_state()
718
973
  },
719
974
  willUnload: false,
720
975
  type: 'enter',
976
+ shallow: false,
721
977
  complete: Promise.resolve()
722
978
  };
723
979
 
724
980
  after_navigate_callbacks.forEach((fn) => fn(navigation));
725
981
  }
726
982
 
983
+ restore_snapshot(current_navigation_index);
984
+ restore_navigation_snapshot(current_history_index);
985
+
727
986
  started = true;
728
987
  }
729
988
 
@@ -734,7 +993,7 @@ async function initialize(result, target, hydrate) {
734
993
  * params: Record<string, string>;
735
994
  * branch: Array<import('./types.js').BranchNode | undefined>;
736
995
  * errors?: Array<import('types').CSRPageNodeLoader | undefined>;
737
- * status: number;
996
+ * status?: number;
738
997
  * error: App.Error | null;
739
998
  * route: import('types').CSRRoute | null;
740
999
  * form?: Record<string, any> | null;
@@ -778,35 +1037,12 @@ async function get_navigation_result_from_branch({
778
1037
  route
779
1038
  },
780
1039
  props: {
781
- // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
782
- constructors: compact(branch).map((branch_node) => branch_node.node.component),
783
- page: clone_page(page)
1040
+ page,
1041
+ tree: /** @type {RenderNode} */ ({})
784
1042
  }
785
1043
  };
786
1044
 
787
- if (errors && __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__) {
788
- let last_idx = -1;
789
- result.props.errors = await Promise.all(
790
- // eslint-disable-next-line @typescript-eslint/await-thenable
791
- branch
792
- .map((b, i) => {
793
- if (i === 0) return undefined; // root layout wraps root error component, not the other way around
794
- if (!b) return null;
795
-
796
- i--;
797
- // Find the closest error component up to the previous branch
798
- while (i > last_idx + 1 && !errors[i]) i -= 1;
799
- last_idx = i;
800
- return errors[i]?.()
801
- .then((e) => e.component)
802
- .catch(() => undefined);
803
- })
804
- // filter out indexes where there was no branch, but keep indexes where there was a branch but no error component
805
- .filter((e) => e !== null)
806
- );
807
- }
808
-
809
- if (error && __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__) {
1045
+ if (error) {
810
1046
  result.props.error = error;
811
1047
  }
812
1048
 
@@ -817,23 +1053,53 @@ async function get_navigation_result_from_branch({
817
1053
  let data = {};
818
1054
  let data_changed = !page;
819
1055
 
820
- let p = 0;
1056
+ let current_node = result.props.tree;
1057
+
1058
+ /** @type {RenderNode | undefined} */
1059
+ let previous_node = props.tree;
821
1060
 
822
- for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) {
1061
+ for (let i = 0; i < branch.length; i += 1) {
823
1062
  const node = branch[i];
824
1063
  const prev = current.branch[i];
825
1064
 
826
- if (node?.data !== prev?.data) data_changed = true;
827
1065
  if (!node) continue;
828
1066
 
829
- data = { ...data, ...node.data };
830
-
831
- // Only set props if the node actually updated. This prevents needless rerenders.
832
- if (data_changed) {
833
- result.props[`data_${p}`] = data;
1067
+ if (
1068
+ // if an ancestor node in this path changed, `data_changed` is already true and the
1069
+ // accumulated `data` differs from the previous render, so we must re-merge
1070
+ data_changed ||
1071
+ !previous_node ||
1072
+ node?.data !== prev?.data
1073
+ ) {
1074
+ current_node.data = { ...data, ...node.data };
1075
+ data_changed = true;
1076
+ } else {
1077
+ // use existing object — prevents effects re-running unnecessarily
1078
+ current_node.data = previous_node.data;
834
1079
  }
835
1080
 
836
- p += 1;
1081
+ data = current_node.data;
1082
+
1083
+ if (i < branch.length - 1) {
1084
+ let next_index = i + 1;
1085
+ while (next_index < branch.length && !branch[next_index]) {
1086
+ next_index += 1;
1087
+ }
1088
+
1089
+ const next = branch[next_index];
1090
+
1091
+ if (next) {
1092
+ const error_loader =
1093
+ errors?.slice(0, next_index + 1).findLast((x) => x) ?? default_error_loader;
1094
+
1095
+ current_node = current_node.child = new RenderNode(
1096
+ next.node.component,
1097
+ (await error_loader()).component
1098
+ );
1099
+
1100
+ previous_node = previous_node?.child;
1101
+ }
1102
+ }
837
1103
  }
838
1104
 
839
1105
  const page_changed =
@@ -851,11 +1117,11 @@ async function get_navigation_result_from_branch({
851
1117
  id: route?.id ?? null
852
1118
  },
853
1119
  state: {},
854
- status,
1120
+ shallow: null,
1121
+ status: status ?? error?.status ?? 200,
855
1122
  url: new URL(url),
856
1123
  form: form ?? null,
857
- // The whole page store is updated, but this way the object reference stays the same
858
- data: data_changed ? data : page.data
1124
+ data
859
1125
  };
860
1126
  }
861
1127
 
@@ -921,7 +1187,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
921
1187
  }
922
1188
  }
923
1189
 
924
- /** @type {import('@sveltejs/kit').LoadEvent} */
1190
+ /** @type {LoadEvent} */
925
1191
  const load_input = {
926
1192
  tracing: { enabled: false, root: noop_span, current: noop_span },
927
1193
  route: new Proxy(route, {
@@ -1134,32 +1400,21 @@ function diff_search_params(old_url, new_url) {
1134
1400
  }
1135
1401
 
1136
1402
  /**
1137
- * @param {Omit<import('./types.js').NavigationFinished['state'], 'branch'> & { error: App.Error }} opts
1138
- * @returns {import('./types.js').NavigationFinished}
1403
+ * @overload
1404
+ * @param {import('./types.js').NavigationIntent & { action_result?: ActionResult }} intent
1405
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1139
1406
  */
1140
- function preload_error({ error, url, route, params }) {
1141
- return {
1142
- type: 'loaded',
1143
- state: {
1144
- error,
1145
- url,
1146
- route,
1147
- params,
1148
- branch: []
1149
- },
1150
- props: {
1151
- page: clone_page(page),
1152
- constructors: []
1153
- }
1154
- };
1155
- }
1156
-
1157
1407
  /**
1158
- * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1408
+ * @overload
1409
+ * @param {import('./types.js').NavigationIntent & { preload: {} }} intent
1159
1410
  * @returns {Promise<import('./types.js').NavigationResult>}
1160
1411
  */
1161
- async function load_route({ id, invalidating, url, params, route, preload }) {
1162
- if (load_cache?.id === id) {
1412
+ /**
1413
+ * @param {import('./types.js').NavigationIntent & { preload?: {}; action_result?: ActionResult }} intent
1414
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1415
+ */
1416
+ async function load_route({ id, invalidating, url, params, route, preload, action_result }) {
1417
+ if (!action_result && load_cache?.id === id) {
1163
1418
  // the preload becomes the real navigation
1164
1419
  preload_tokens.delete(load_cache.token);
1165
1420
  return load_cache.promise;
@@ -1168,6 +1423,8 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1168
1423
  const { errors, layouts, leaf } = route;
1169
1424
 
1170
1425
  const loaders = [...layouts, leaf];
1426
+ const leaf_index = loaders.length - 1;
1427
+ const action_error = action_result?.type === 'error';
1171
1428
 
1172
1429
  // preload modules to avoid waterfall, but handle rejections
1173
1430
  // so they don't get reported to Sentry et al (we don't need
@@ -1178,13 +1435,16 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1178
1435
  /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
1179
1436
  let server_data = null;
1180
1437
  const url_changed = current.url ? id !== get_page_key(current.url) : false;
1181
- const route_changed = current.route ? route.id !== current.route.id : false;
1438
+ // current.route is null after an error-page render, so a missing route counts as changed
1439
+ const route_changed = !current.route || route.id !== current.route.id;
1182
1440
  const search_params_changed = diff_search_params(current.url, url);
1183
1441
 
1184
1442
  let parent_invalid = false;
1185
1443
 
1186
1444
  if (__SVELTEKIT_HAS_SERVER_LOAD__) {
1187
1445
  const invalid_server_nodes = loaders.map((loader, i) => {
1446
+ if (action_error && i === leaf_index) return false;
1447
+
1188
1448
  const previous = current.branch[i];
1189
1449
 
1190
1450
  const invalid =
@@ -1213,12 +1473,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1213
1473
  } catch (error) {
1214
1474
  const handled_error = await handle_error(error, { url, params, route: { id } });
1215
1475
 
1216
- if (preload_tokens.has(preload)) {
1217
- return preload_error({ error: handled_error, url, params, route });
1476
+ if (preload && preload_tokens.has(preload)) {
1477
+ throw handled_error;
1218
1478
  }
1219
1479
 
1220
1480
  return load_root_error_page({
1221
- status: get_status(error),
1222
1481
  error: handled_error,
1223
1482
  url,
1224
1483
  route
@@ -1236,7 +1495,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1236
1495
  let parent_changed = false;
1237
1496
 
1238
1497
  const branch_promises = loaders.map(async (loader, i) => {
1239
- if (!loader) return;
1498
+ if (!loader || (action_error && i === leaf_index)) return;
1240
1499
 
1241
1500
  /** @type {import('./types.js').BranchNode | undefined} */
1242
1501
  const previous = current.branch[i];
@@ -1261,7 +1520,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1261
1520
 
1262
1521
  if (server_data_node?.type === 'error') {
1263
1522
  // rethrow and catch below
1264
- throw server_data_node;
1523
+ throw new HandledHttpError(server_data_node.error);
1265
1524
  }
1266
1525
 
1267
1526
  return load_node({
@@ -1299,34 +1558,27 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1299
1558
  if (err instanceof Redirect) {
1300
1559
  return {
1301
1560
  type: 'redirect',
1561
+ status: err.status,
1302
1562
  location: err.location
1303
1563
  };
1304
1564
  }
1305
1565
 
1306
- if (preload_tokens.has(preload)) {
1307
- return preload_error({
1308
- error: await handle_error(err, { params, url, route: { id: route.id } }),
1309
- url,
1310
- params,
1311
- route
1312
- });
1566
+ if (preload && preload_tokens.has(preload)) {
1567
+ throw await handle_error(err, { params, url, route: { id: route.id } });
1313
1568
  }
1314
1569
 
1315
- let status = get_status(err);
1316
1570
  /** @type {App.Error} */
1317
1571
  let error;
1318
1572
 
1319
1573
  if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
1320
1574
  // this is the server error rethrown above, reconstruct but don't invoke
1321
1575
  // the client error handler; it should've already been handled on the server
1322
- status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
1323
1576
  error = /** @type {import('types').ServerErrorNode} */ (err).error;
1324
1577
  } else if (err instanceof HttpError) {
1325
- error = err.body;
1578
+ error = await handle_error(err, { params, url, route: { id: route.id } });
1326
1579
  } else {
1327
1580
  // Referenced node could have been removed due to redeploy, check
1328
- const updated = await stores.updated.check();
1329
- if (updated) {
1581
+ if (await updated.check()) {
1330
1582
  // Before reloading, try to update the service worker if it exists
1331
1583
  await update_service_worker();
1332
1584
  return await native_navigation(url);
@@ -1335,20 +1587,15 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1335
1587
  error = await handle_error(err, { params, url, route: { id: route.id } });
1336
1588
  }
1337
1589
 
1338
- const error_load = await load_nearest_error_page(i, branch, errors);
1339
- if (error_load) {
1340
- return get_navigation_result_from_branch({
1341
- url,
1342
- params,
1343
- branch: branch.slice(0, error_load.idx).concat(error_load.node),
1344
- errors,
1345
- status,
1346
- error,
1347
- route
1348
- });
1349
- } else {
1350
- return await server_fallback(url, { id: route.id }, error, status);
1351
- }
1590
+ return load_route_error({
1591
+ i,
1592
+ branch,
1593
+ errors,
1594
+ error,
1595
+ url,
1596
+ params,
1597
+ route
1598
+ });
1352
1599
  }
1353
1600
  } else {
1354
1601
  // push an empty slot so we can rewind past gaps to the
@@ -1357,16 +1604,37 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1357
1604
  }
1358
1605
  }
1359
1606
 
1607
+ if (action_result?.type === 'error') {
1608
+ // render the nearest error boundary of the route the action belongs to,
1609
+ // as a native form submission would
1610
+ return load_route_error({
1611
+ i: loaders.length,
1612
+ branch,
1613
+ errors,
1614
+ error: action_result.error,
1615
+ status: action_result.status,
1616
+ url,
1617
+ params,
1618
+ route
1619
+ });
1620
+ }
1621
+
1360
1622
  return get_navigation_result_from_branch({
1361
1623
  url,
1362
1624
  params,
1363
1625
  branch,
1364
1626
  errors,
1365
- status: 200,
1366
1627
  error: null,
1367
1628
  route,
1368
- // Reset `form` on navigation, but not invalidation
1369
- form: invalidating ? undefined : null
1629
+ status: action_result?.status,
1630
+ // `error` results returned above, `redirect` results are never passed here
1631
+ form:
1632
+ action_result?.type === 'success' || action_result?.type === 'failure'
1633
+ ? (action_result.data ?? null)
1634
+ : // Reset `form` on navigation, but not invalidation
1635
+ invalidating
1636
+ ? undefined
1637
+ : null
1370
1638
  });
1371
1639
  }
1372
1640
 
@@ -1401,14 +1669,42 @@ async function load_nearest_error_page(i, branch, errors) {
1401
1669
 
1402
1670
  /**
1403
1671
  * @param {{
1404
- * status: number;
1672
+ * i: number;
1673
+ * branch: Array<import('./types.js').BranchNode | undefined>;
1674
+ * errors: Array<import('types').CSRPageNodeLoader | undefined>;
1675
+ * error: App.Error;
1676
+ * status?: number;
1677
+ * url: URL;
1678
+ * params: Record<string, string>;
1679
+ * route: import('./types.js').NavigationIntent['route'];
1680
+ * }} opts
1681
+ */
1682
+ async function load_route_error({ i, branch, errors, error, status, url, params, route }) {
1683
+ const error_load = await load_nearest_error_page(i, branch, errors);
1684
+ if (error_load) {
1685
+ return get_navigation_result_from_branch({
1686
+ url,
1687
+ params,
1688
+ branch: branch.slice(0, error_load.idx).concat(error_load.node),
1689
+ errors,
1690
+ error,
1691
+ status,
1692
+ route
1693
+ });
1694
+ }
1695
+
1696
+ return server_fallback(url, { id: route.id }, error);
1697
+ }
1698
+
1699
+ /**
1700
+ * @param {{
1405
1701
  * error: App.Error;
1406
1702
  * url: URL;
1407
1703
  * route: { id: string | null }
1408
1704
  * }} opts
1409
- * @returns {Promise<import('./types.js').NavigationFinished>}
1705
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>} returns `undefined` in case of a redirect
1410
1706
  */
1411
- async function load_root_error_page({ status, error, url, route }) {
1707
+ async function load_root_error_page({ error, url, route }) {
1412
1708
  /** @type {Record<string, string>} */
1413
1709
  const params = {}; // error page does not have params
1414
1710
 
@@ -1432,11 +1728,15 @@ async function load_root_error_page({ status, error, url, route }) {
1432
1728
  }
1433
1729
 
1434
1730
  server_data_node = server_data.nodes[0] ?? null;
1435
- } catch {
1731
+ } catch (e) {
1436
1732
  // at this point we have no choice but to fall back to the server, if it wouldn't
1437
- // bring us right back here, turning this into an endless loop
1438
- if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
1439
- await native_navigation(url);
1733
+ // bring us right back here, turning this into an endless loop.
1734
+ // if __data.json returned 404, the route doesn't exist don't reload or we loop
1735
+ if (
1736
+ !(e instanceof HttpError && e.status === 404) &&
1737
+ (url.origin !== origin || url.pathname !== location.pathname || hydrated)
1738
+ ) {
1739
+ return await native_navigation(url);
1440
1740
  }
1441
1741
  }
1442
1742
  }
@@ -1465,18 +1765,30 @@ async function load_root_error_page({ status, error, url, route }) {
1465
1765
  url,
1466
1766
  params,
1467
1767
  branch: [root_layout, root_error],
1468
- status,
1469
1768
  error,
1470
1769
  errors: [],
1471
1770
  route: null
1472
1771
  });
1473
1772
  } catch (error) {
1773
+ // client-side navigation if the root layout loader throws a redirect while
1774
+ // rendering the default error page
1474
1775
  if (error instanceof Redirect) {
1475
- // @ts-expect-error TODO investigate this
1476
- return _goto(new URL(error.location, location.href), {}, 0);
1776
+ await _goto(new URL(error.location, location.href));
1777
+ return;
1477
1778
  }
1478
1779
 
1479
- // TODO: this falls back to the server when a server exists, but what about SPA mode?
1780
+ // otherwise, render the static error page
1781
+ const error_template = await app.get_error_template();
1782
+ const handled = await handle_error(error, { url, params, route });
1783
+ const message = String(handled?.message ?? '')
1784
+ .replace(/&/g, '&amp;')
1785
+ .replace(/</g, '&lt;')
1786
+ .replace(/>/g, '&gt;');
1787
+ const html = error_template({ status: handled.status, message });
1788
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
1789
+ document.documentElement.replaceChild(document.adoptNode(parsed.head), document.head);
1790
+ document.documentElement.replaceChild(document.adoptNode(parsed.body), document.body);
1791
+
1480
1792
  throw error;
1481
1793
  }
1482
1794
  }
@@ -1566,7 +1878,7 @@ export async function get_navigation_intent(url, invalidating) {
1566
1878
  id: get_page_key(url),
1567
1879
  invalidating,
1568
1880
  route,
1569
- params: decode_params(params),
1881
+ params,
1570
1882
  url
1571
1883
  };
1572
1884
  }
@@ -1583,7 +1895,7 @@ export async function get_navigation_intent(url, invalidating) {
1583
1895
  return {
1584
1896
  id: get_page_key(url),
1585
1897
  invalidating,
1586
- route: parse_server_route(route, app.nodes),
1898
+ route: parse_and_cache_server_route(route),
1587
1899
  params,
1588
1900
  url
1589
1901
  };
@@ -1607,17 +1919,19 @@ function get_page_key(url) {
1607
1919
  /**
1608
1920
  * @param {{
1609
1921
  * url: URL;
1610
- * type: import('@sveltejs/kit').Navigation["type"];
1922
+ * type: Navigation["type"];
1611
1923
  * intent?: import('./types.js').NavigationIntent;
1612
1924
  * delta?: number;
1613
1925
  * event?: PopStateEvent | MouseEvent;
1614
1926
  * scroll?: { x: number, y: number };
1927
+ * shallow?: boolean;
1928
+ * target?: { params: Record<string, string> | null; route: { id: string } | null; url: URL };
1615
1929
  * }} opts
1616
1930
  */
1617
- function _before_navigate({ url, type, intent, delta, event, scroll }) {
1931
+ function _before_navigate({ url, type, intent, delta, event, scroll, shallow = false, target }) {
1618
1932
  let should_block = false;
1619
1933
 
1620
- const nav = create_navigation(current, intent, url, type, scroll ?? null);
1934
+ const nav = create_navigation(current, intent, url, type, scroll ?? null, shallow, target);
1621
1935
 
1622
1936
  if (nav.navigation.type === 'popstate' && delta !== undefined) {
1623
1937
  nav.navigation.delta = delta;
@@ -1638,6 +1952,9 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1638
1952
 
1639
1953
  if (!is_navigating) {
1640
1954
  // Don't run the event during redirects
1955
+ // TODO this isn't fully right: if you do a goto(...) while another goto(...) is in progress,
1956
+ // or you click a link while a navigation is in progress, the beforNavigate calls are not triggered,
1957
+ // and maybe they should be?
1641
1958
  before_navigate_callbacks.forEach((fn) => fn(cancellable));
1642
1959
  }
1643
1960
 
@@ -1646,42 +1963,49 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1646
1963
 
1647
1964
  /**
1648
1965
  * @param {{
1649
- * type: import('@sveltejs/kit').NavigationType;
1966
+ * type: NavigationType;
1650
1967
  * url: URL;
1651
1968
  * popped?: {
1652
1969
  * state: Record<string, any>;
1653
- * scroll: { x: number, y: number };
1970
+ * scroll?: { x: number, y: number };
1654
1971
  * delta: number;
1972
+ * shallow: { params: Record<string, string> | null; route: { id: string } | null; url: URL } | null;
1655
1973
  * };
1656
- * keepfocus?: boolean;
1657
- * noscroll?: boolean;
1974
+ * reset?: boolean;
1658
1975
  * replace_state?: boolean;
1659
1976
  * state?: Record<string, any>;
1977
+ * persist_state?: boolean;
1660
1978
  * redirect_count?: number;
1661
1979
  * nav_token?: {};
1662
1980
  * accept?: () => void;
1663
1981
  * block?: () => void;
1664
- * event?: Event
1982
+ * event?: Event;
1983
+ * intent?: NavigationIntent | undefined;
1984
+ * action_result?: ActionResult;
1665
1985
  * }} opts
1986
+ * @returns {Promise<void>}
1666
1987
  */
1667
1988
  async function navigate({
1668
1989
  type,
1669
1990
  url,
1670
1991
  popped,
1671
- keepfocus,
1672
- noscroll,
1992
+ reset = true,
1673
1993
  replace_state,
1674
1994
  state = {},
1995
+ persist_state = false,
1675
1996
  redirect_count = 0,
1676
1997
  nav_token = {},
1677
1998
  accept = noop,
1678
1999
  block = noop,
1679
- event
2000
+ event,
2001
+ intent,
2002
+ action_result
1680
2003
  }) {
1681
- const prev_token = token;
1682
- token = nav_token;
2004
+ const prev_token = navigation_token;
2005
+ const prev_invalidation_token = invalidation_token;
2006
+ navigation_token = invalidation_token = nav_token;
1683
2007
 
1684
- const intent = await get_navigation_intent(url, false);
2008
+ intent ??= await get_navigation_intent(url, false);
1685
2009
  const nav =
1686
2010
  type === 'enter'
1687
2011
  ? create_navigation(current, intent, url, type)
@@ -1691,29 +2015,33 @@ async function navigate({
1691
2015
  delta: popped?.delta,
1692
2016
  intent,
1693
2017
  scroll: popped?.scroll,
2018
+ shallow: !!popped?.shallow,
2019
+ target: popped?.shallow ?? undefined,
1694
2020
  // @ts-ignore
1695
2021
  event
1696
2022
  });
1697
2023
 
1698
2024
  if (!nav) {
1699
2025
  block();
1700
- if (token === nav_token) token = prev_token;
2026
+ if (navigation_token === nav_token) navigation_token = prev_token;
2027
+ if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
1701
2028
  return;
1702
2029
  }
1703
2030
 
1704
2031
  // store this before calling `accept()`, which may change the index
1705
2032
  const previous_history_index = current_history_index;
1706
2033
  const previous_navigation_index = current_navigation_index;
2034
+ const previous_snapshot_registrations = current_registrations();
1707
2035
 
1708
2036
  accept();
1709
2037
 
1710
2038
  is_navigating = true;
1711
2039
 
1712
2040
  if (started && nav.navigation.type !== 'enter') {
1713
- stores.navigating.set((navigating.current = nav.navigation));
2041
+ navigating.current = nav.navigation;
1714
2042
  }
1715
2043
 
1716
- let navigation_result = intent && (await load_route(intent));
2044
+ let navigation_result = intent && (await load_route({ ...intent, action_result }));
1717
2045
 
1718
2046
  if (!navigation_result) {
1719
2047
  if (is_external_url(url, base, app.hash)) {
@@ -1735,7 +2063,6 @@ async function navigate({
1735
2063
  route: { id: null }
1736
2064
  }
1737
2065
  ),
1738
- 404,
1739
2066
  replace_state
1740
2067
  );
1741
2068
  } else {
@@ -1750,7 +2077,6 @@ async function navigate({
1750
2077
  params: {},
1751
2078
  route: { id: null }
1752
2079
  }),
1753
- 404,
1754
2080
  replace_state
1755
2081
  );
1756
2082
  }
@@ -1761,11 +2087,13 @@ async function navigate({
1761
2087
  url = intent?.url || url;
1762
2088
 
1763
2089
  // abort if user navigated during update
1764
- if (token !== nav_token) {
2090
+ if (navigation_token !== nav_token) {
1765
2091
  nav.reject(new Error('navigation aborted'));
1766
- return false;
2092
+ return;
1767
2093
  }
1768
2094
 
2095
+ if (!navigation_result) return;
2096
+
1769
2097
  if (navigation_result.type === 'redirect') {
1770
2098
  // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1771
2099
  if (redirect_count < 20) {
@@ -1773,10 +2101,10 @@ async function navigate({
1773
2101
  type,
1774
2102
  url: new URL(navigation_result.location, url),
1775
2103
  popped,
1776
- keepfocus,
1777
- noscroll,
2104
+ reset,
1778
2105
  replace_state,
1779
2106
  state,
2107
+ persist_state,
1780
2108
  redirect_count: redirect_count + 1,
1781
2109
  nav_token
1782
2110
  });
@@ -1786,7 +2114,6 @@ async function navigate({
1786
2114
  }
1787
2115
 
1788
2116
  navigation_result = await load_root_error_page({
1789
- status: 500,
1790
2117
  error: await handle_error(new Error('Redirect loop'), {
1791
2118
  url,
1792
2119
  params: {},
@@ -1795,12 +2122,13 @@ async function navigate({
1795
2122
  url,
1796
2123
  route: { id: null }
1797
2124
  });
2125
+
2126
+ if (!navigation_result) return;
1798
2127
  } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1799
- const updated = await stores.updated.check();
1800
- if (updated) {
2128
+ if (await updated.check()) {
1801
2129
  // Before reloading, try to update the service worker if it exists
1802
2130
  await update_service_worker();
1803
- await native_navigation(url, replace_state);
2131
+ return await native_navigation(url, replace_state);
1804
2132
  }
1805
2133
  }
1806
2134
 
@@ -1810,28 +2138,48 @@ async function navigate({
1810
2138
 
1811
2139
  updating = true;
1812
2140
 
1813
- update_scroll_positions(previous_history_index);
2141
+ capture_scroll(previous_history_index);
1814
2142
  capture_snapshot(previous_navigation_index);
2143
+ if (replace_state) {
2144
+ delete_navigation_snapshot(previous_history_index);
2145
+ } else {
2146
+ capture_navigation_snapshot(previous_history_index);
2147
+ }
1815
2148
 
1816
2149
  // ensure the url pathname matches the page's trailing slash option
1817
2150
  if (navigation_result.props.page.url.pathname !== url.pathname) {
1818
2151
  url.pathname = navigation_result.props.page.url.pathname;
1819
2152
  }
1820
2153
 
1821
- state = popped ? popped.state : state;
1822
-
1823
- if (!popped) {
1824
- // this is a new navigation, rather than a popstate
2154
+ if (popped) {
2155
+ state = popped.state;
2156
+ } else {
2157
+ // we immediately serialize-then-parse to ensure that the value is
2158
+ // serializable, and to prevent the developer from dangerously
2159
+ // relying on the identity of the serialized objects
2160
+ const serialized_state = stringify(state);
2161
+ state = parse(serialized_state);
2162
+
2163
+ // Store the serialized state so the browser history can preserve custom transport values.
2164
+ // This is a new navigation, rather than a popstate.
1825
2165
  const change = replace_state ? 0 : 1;
2166
+ if (type !== 'enter') {
2167
+ if (reset) current_reset_index += 1;
2168
+ }
1826
2169
 
1827
2170
  const entry = {
1828
- [HISTORY_INDEX]: (current_history_index += change),
1829
- [NAVIGATION_INDEX]: (current_navigation_index += change),
1830
- [STATES_KEY]: state
2171
+ [HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
2172
+ historyIndex: (current_history_index += change),
2173
+ navigationIndex: (current_navigation_index += change),
2174
+ state: serialized_state,
2175
+ persistState: persist_state,
2176
+ resetIndex: current_reset_index
2177
+ })
1831
2178
  };
1832
2179
 
1833
2180
  const fn = replace_state ? history.replaceState : history.pushState;
1834
2181
  fn.call(history, entry, '', url);
2182
+ set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
1835
2183
 
1836
2184
  if (!replace_state) {
1837
2185
  clear_onward_history(current_history_index, current_navigation_index);
@@ -1850,6 +2198,7 @@ async function navigate({
1850
2198
  }
1851
2199
 
1852
2200
  navigation_result.props.page.state = state;
2201
+ navigation_result.props.page.shallow = popped?.shallow ?? null;
1853
2202
 
1854
2203
  /**
1855
2204
  * @type {Promise<void> | undefined}
@@ -1859,9 +2208,7 @@ async function navigate({
1859
2208
  const after_navigate = (
1860
2209
  await Promise.all(
1861
2210
  // eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
1862
- Array.from(on_navigate_callbacks, (fn) =>
1863
- fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
1864
- )
2211
+ Array.from(on_navigate_callbacks, (fn) => fn(/** @type {OnNavigate} */ (nav.navigation)))
1865
2212
  )
1866
2213
  ).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
1867
2214
 
@@ -1880,7 +2227,13 @@ async function navigate({
1880
2227
  }
1881
2228
 
1882
2229
  // Type-casts are save because we know this resolved a proper SvelteKit route
1883
- const target = /** @type {import('@sveltejs/kit').NavigationTarget} */ (nav.navigation.to);
2230
+ const target = popped?.shallow
2231
+ ? {
2232
+ params: navigation_result.state.params,
2233
+ route: { id: navigation_result.state.route?.id ?? null },
2234
+ url: navigation_result.state.url
2235
+ }
2236
+ : /** @type {NavigationTarget} */ (nav.navigation.to);
1884
2237
  current = {
1885
2238
  ...navigation_result.state,
1886
2239
  nav: {
@@ -1895,20 +2248,33 @@ async function navigate({
1895
2248
  navigation_result.props.page.url = url;
1896
2249
  }
1897
2250
 
2251
+ // Remove focus before updating the component tree, so that blur/focusout
2252
+ // handlers fire while the old component's data is still valid (#14575)
2253
+ blur_active_element(reset);
2254
+
1898
2255
  const fork = load_cache_fork && (await load_cache_fork);
1899
2256
 
1900
2257
  if (fork) {
1901
2258
  commit_promise = fork.commit();
2259
+ // `fork.commit()` applies the preloaded state synchronously before the
2260
+ // first `await`, so reset any previously-failed boundaries now so the
2261
+ // stale `+error.svelte` is torn down. See sveltejs/kit#15694.
2262
+ for (const reset_boundary of resetters) {
2263
+ reset_boundary();
2264
+ }
2265
+ resetters.clear();
1902
2266
  } else {
1903
- rendering_error = null; // TODO this can break with forks, rethink for SvelteKit 3 where we can assume Svelte 5
1904
- root.$set(navigation_result.props);
1905
- // Check for sync rendering error
1906
- if (rendering_error) {
1907
- Object.assign(navigation_result.props.page, rendering_error);
2267
+ apply_navigation_result(navigation_result);
2268
+
2269
+ // Reset any boundaries that failed on a previous navigation now that the
2270
+ // new props are applied, otherwise the stale `+error.svelte` stays
2271
+ // mounted above the new route's content. See sveltejs/kit#15694.
2272
+ for (const reset_boundary of resetters) {
2273
+ reset_boundary();
1908
2274
  }
1909
- update(navigation_result.props.page);
2275
+ resetters.clear();
1910
2276
 
1911
- commit_promise = svelte.settled?.();
2277
+ commit_promise = settled();
1912
2278
  }
1913
2279
 
1914
2280
  has_navigated = true;
@@ -1920,75 +2286,32 @@ async function navigate({
1920
2286
 
1921
2287
  await commit_promise;
1922
2288
 
1923
- // TODO 3.0 remote — the double tick is probably necessary because
1924
- // of some store shenanigans. `settled()` and `f.commit()`
1925
- // should resolve after DOM updates in newer versions
1926
- await svelte.tick();
1927
- await svelte.tick();
1928
-
1929
- if (token !== nav_token) {
2289
+ if (navigation_token !== nav_token) {
1930
2290
  // a new navigation happened while we were waiting for the DOM to update, so abort
1931
2291
  nav.reject(new Error('navigation aborted'));
1932
- return false;
2292
+ return;
1933
2293
  }
1934
2294
 
1935
- // Check for async rendering error
1936
- if (navigation_result.props.page && rendering_error) {
1937
- Object.assign(navigation_result.props.page, rendering_error);
1938
- }
2295
+ reset_scroll_and_focus(url, reset ? popped?.scroll : scroll_state(), reset, activeElement);
1939
2296
 
1940
- // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
1941
- /** @type {Element | null | ''} */
1942
- let deep_linked = null;
2297
+ is_navigating = false;
1943
2298
 
1944
- if (autoscroll) {
1945
- const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1946
- if (scroll) {
1947
- scrollTo(scroll.x, scroll.y);
1948
- } else if ((deep_linked = url.hash && document.getElementById(get_id(url)))) {
1949
- // Here we use `scrollIntoView` on the element instead of `scrollTo`
1950
- // because it natively supports the `scroll-margin` and `scroll-behavior`
1951
- // CSS properties.
1952
- deep_linked.scrollIntoView();
1953
- } else {
1954
- scrollTo(0, 0);
1955
- }
1956
- }
2299
+ nav.fulfil(undefined);
1957
2300
 
1958
- const changed_focus =
1959
- // reset focus only if any manual focus management didn't override it
1960
- document.activeElement !== activeElement &&
1961
- // also refocus when activeElement is body already because the
1962
- // focus event might not have been fired on it yet
1963
- document.activeElement !== document.body;
1964
-
1965
- if (!keepfocus && !changed_focus) {
1966
- // We don't need to manually restore the scroll position if we're navigating
1967
- // to a fragment identifier. It is automatically done for us when we set the
1968
- // sequential navigation starting point with `location.replace`
1969
- reset_focus(url, !deep_linked);
2301
+ // Update to.scroll to the actual scroll position after navigation completed
2302
+ if (nav.navigation.to) {
2303
+ nav.navigation.to.scroll = scroll_state();
1970
2304
  }
1971
2305
 
1972
- autoscroll = true;
1973
-
1974
- is_navigating = false;
2306
+ after_navigate_callbacks.forEach((fn) => fn(/** @type {AfterNavigate} */ (nav.navigation)));
1975
2307
 
1976
2308
  if (type === 'popstate') {
1977
2309
  restore_snapshot(current_navigation_index);
1978
2310
  }
2311
+ // new and replaced entries have no stored values, so this only resets there
2312
+ restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
1979
2313
 
1980
- nav.fulfil(undefined);
1981
-
1982
- // Update to.scroll to the actual scroll position after navigation completed
1983
- if (nav.navigation.to) {
1984
- nav.navigation.to.scroll = scroll_state();
1985
- }
1986
-
1987
- after_navigate_callbacks.forEach((fn) =>
1988
- fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
1989
- );
1990
-
1991
- stores.navigating.set((navigating.current = null));
2314
+ navigating.current = null;
1992
2315
 
1993
2316
  updating = false;
1994
2317
  }
@@ -1998,23 +2321,21 @@ async function navigate({
1998
2321
  * @param {URL} url
1999
2322
  * @param {{ id: string | null }} route
2000
2323
  * @param {App.Error} error
2001
- * @param {number} status
2002
2324
  * @param {boolean} [replace_state]
2003
- * @returns {Promise<import('./types.js').NavigationFinished>}
2325
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>}
2004
2326
  */
2005
- async function server_fallback(url, route, error, status, replace_state) {
2327
+ async function server_fallback(url, route, error, replace_state) {
2006
2328
  if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
2007
2329
  // We would reload the same page we're currently on, which isn't hydrated,
2008
2330
  // which means no SSR, which means we would end up in an endless loop
2009
2331
  return await load_root_error_page({
2010
- status,
2011
2332
  error,
2012
2333
  url,
2013
2334
  route
2014
2335
  });
2015
2336
  }
2016
2337
 
2017
- if (DEV && status !== 404) {
2338
+ if (DEV && error.status !== 404) {
2018
2339
  console.error(
2019
2340
  'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
2020
2341
  );
@@ -2043,22 +2364,63 @@ let current_a = { element: undefined, href: undefined };
2043
2364
  function setup_preload() {
2044
2365
  /** @type {NodeJS.Timeout} */
2045
2366
  let mousemove_timeout;
2367
+ /** @type {HTMLAnchorElement | SVGAElement | undefined} */
2368
+ let hovered_a;
2046
2369
  /** @type {PreloadDataPriority} */
2047
2370
  let current_priority;
2048
2371
 
2049
- container.addEventListener('mousemove', (event) => {
2050
- const target = /** @type {Element} */ (event.target);
2372
+ function clear_hover_preload() {
2373
+ clearTimeout(mousemove_timeout);
2374
+ hovered_a?.removeEventListener('mousemove', start_hover_preload);
2375
+ hovered_a?.removeEventListener('mouseleave', clear_hover_preload);
2376
+ hovered_a = undefined;
2377
+ }
2051
2378
 
2379
+ function start_hover_preload() {
2052
2380
  clearTimeout(mousemove_timeout);
2053
2381
  mousemove_timeout = setTimeout(() => {
2054
- void preload(target, PRELOAD_PRIORITIES.hover);
2382
+ if (!hovered_a) return;
2383
+ void preload(hovered_a, PRELOAD_PRIORITIES.hover);
2055
2384
  }, 20);
2385
+ }
2386
+
2387
+ // Use mouseover initially instead of mousemove to avoid cluttering the event queue
2388
+ container.addEventListener('mouseover', (event) => {
2389
+ if (!(event.target instanceof Element)) return;
2390
+
2391
+ const a = find_anchor(event.target, container);
2392
+ if (!a || a === hovered_a) return;
2393
+
2394
+ clear_hover_preload();
2395
+ hovered_a = a;
2396
+
2397
+ const options = get_router_options(a);
2398
+
2399
+ if (
2400
+ options.preload_code < PRELOAD_PRIORITIES.hover &&
2401
+ options.preload_data < PRELOAD_PRIORITIES.hover
2402
+ ) {
2403
+ // don't add event listeners if no preloading will happen
2404
+ return;
2405
+ }
2406
+
2407
+ // Instead of just preloading right away, we start a mousemove listener to implement
2408
+ // "mouse comes to a rest" behavior. This avoid false positives when you just move
2409
+ // your mouse across the screen and happen to pass over a link.
2410
+ a.addEventListener('mousemove', start_hover_preload);
2411
+ a.addEventListener('mouseleave', clear_hover_preload, { once: true });
2412
+
2413
+ start_hover_preload();
2056
2414
  });
2057
2415
 
2058
2416
  /** @param {Event} event */
2059
2417
  function tap(event) {
2060
2418
  if (event.defaultPrevented) return;
2061
- void preload(/** @type {Element} */ (event.composedPath()[0]), PRELOAD_PRIORITIES.tap);
2419
+
2420
+ const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
2421
+ if (!a) return;
2422
+
2423
+ void preload(a, PRELOAD_PRIORITIES.tap);
2062
2424
  }
2063
2425
 
2064
2426
  container.addEventListener('mousedown', tap);
@@ -2077,12 +2439,10 @@ function setup_preload() {
2077
2439
  );
2078
2440
 
2079
2441
  /**
2080
- * @param {Element} element
2442
+ * @param {HTMLAnchorElement | SVGAElement} a
2081
2443
  * @param {PreloadDataPriority} priority
2082
2444
  */
2083
- async function preload(element, priority) {
2084
- const a = find_anchor(element, container);
2085
-
2445
+ async function preload(a, priority) {
2086
2446
  // we don't want to preload data again if the user has already hovered/tapped
2087
2447
  const interacted =
2088
2448
  a === current_a.element && a?.href === current_a.href && priority >= current_priority;
@@ -2106,15 +2466,13 @@ function setup_preload() {
2106
2466
  if (!intent) return;
2107
2467
 
2108
2468
  if (DEV) {
2109
- void _preload_data(intent).then((result) => {
2110
- if (result.type === 'loaded' && result.state.error) {
2111
- console.warn(
2112
- `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` +
2113
- 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
2114
- 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
2115
- 'See https://svelte.dev/docs/kit/link-options for more info'
2116
- );
2117
- }
2469
+ void _preload_data(intent).catch((error) => {
2470
+ console.warn(
2471
+ `Preloading data for ${intent.url.pathname} failed with the following error: ${error.message}\n` +
2472
+ 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
2473
+ 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
2474
+ 'See https://svelte.dev/docs/kit/link-options for more info'
2475
+ );
2118
2476
  });
2119
2477
  } else {
2120
2478
  void _preload_data(intent);
@@ -2152,25 +2510,41 @@ function setup_preload() {
2152
2510
 
2153
2511
  /**
2154
2512
  * @param {unknown} error
2155
- * @param {import('@sveltejs/kit').NavigationEvent} event
2156
- * @returns {import('types').MaybePromise<App.Error>}
2513
+ * @param {NavigationEvent} event
2514
+ * @returns {Promise<App.Error>}
2157
2515
  */
2158
- function handle_error(error, event) {
2159
- if (error instanceof HttpError) {
2516
+ export async function handle_error(error, event) {
2517
+ if (error instanceof HandledHttpError) {
2160
2518
  return error.body;
2161
2519
  }
2162
2520
 
2163
- if (DEV) {
2521
+ /** @type {import('@sveltejs/kit/hooks').ClientCaughtError} */
2522
+ let caught;
2523
+
2524
+ if (error instanceof HttpError) {
2525
+ caught = { kind: 'app', error: error.body };
2526
+ } else if (error instanceof SvelteKitError) {
2527
+ caught = { kind: 'framework', error: { status: error.status, message: error.text } };
2528
+ } else {
2529
+ caught = { kind: 'unknown', error };
2530
+ }
2531
+
2532
+ if (DEV && caught.kind !== 'app') {
2164
2533
  errored = true;
2165
2534
  console.warn('The next HMR update will cause the page to reload');
2166
2535
  }
2167
2536
 
2168
- const status = get_status(error);
2169
- const message = get_message(error);
2537
+ const fallback =
2538
+ caught.kind === 'unknown' ? { status: 500, message: 'Internal Error' } : caught.error;
2170
2539
 
2171
- return (
2172
- app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
2173
- );
2540
+ const input = { ...caught, event };
2541
+ if (DEV) add_deprecated_handle_error_properties(input, fallback);
2542
+
2543
+ const app_error = await app.hooks.handleError(input);
2544
+
2545
+ // the hook returns only the properties it wants to override; anything it omits
2546
+ // (including by returning nothing at all) is inherited from the caught error
2547
+ return { ...fallback, ...app_error };
2174
2548
  }
2175
2549
 
2176
2550
  /**
@@ -2192,7 +2566,7 @@ function add_navigation_callback(callbacks, callback) {
2192
2566
  * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a URL.
2193
2567
  *
2194
2568
  * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
2195
- * @param {(navigation: import('@sveltejs/kit').AfterNavigate) => void} callback
2569
+ * @param {(navigation: AfterNavigate) => void} callback
2196
2570
  * @returns {void}
2197
2571
  */
2198
2572
  export function afterNavigate(callback) {
@@ -2209,7 +2583,7 @@ export function afterNavigate(callback) {
2209
2583
  * If the navigation will (if not cancelled) cause the document to unload — in other words `'leave'` navigations and `'link'` navigations where `navigation.to.route === null` — `navigation.willUnload` is `true`.
2210
2584
  *
2211
2585
  * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
2212
- * @param {(navigation: import('@sveltejs/kit').BeforeNavigate) => void} callback
2586
+ * @param {(navigation: BeforeNavigate) => void} callback
2213
2587
  * @returns {void}
2214
2588
  */
2215
2589
  export function beforeNavigate(callback) {
@@ -2224,7 +2598,7 @@ export function beforeNavigate(callback) {
2224
2598
  * If a function (or a `Promise` that resolves to a function) is returned from the callback, it will be called once the DOM has updated.
2225
2599
  *
2226
2600
  * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
2227
- * @param {(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>} callback
2601
+ * @param {(navigation: OnNavigate) => import('types').MaybePromise<(() => void) | void>} callback
2228
2602
  * @returns {void}
2229
2603
  */
2230
2604
  export function onNavigate(callback) {
@@ -2250,40 +2624,103 @@ export function disableScrollHandling() {
2250
2624
  }
2251
2625
  }
2252
2626
 
2627
+ let warned_on_invalidate_all = false;
2628
+ let warned_on_replace_state = false;
2629
+ let warned_on_push_state = false;
2630
+ let warned_on_replace_state_function = false;
2631
+
2253
2632
  /**
2254
- * Allows you to navigate programmatically to a given route, with options such as keeping the current element focused.
2255
- * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
2633
+ * @param {string | URL} url
2634
+ * @param {'goto' | 'pushState' | 'replaceState'} caller
2635
+ */
2636
+ async function resolve_intent(url, caller) {
2637
+ const resolved = new URL(resolve_url(url));
2638
+
2639
+ if (resolved.origin !== origin) {
2640
+ throw new Error(
2641
+ DEV
2642
+ ? `Cannot use \`${caller}\` with an external URL. Use \`window.location = "${url}"\` instead`
2643
+ : `${caller}: invalid URL`
2644
+ );
2645
+ }
2646
+
2647
+ const intent = await get_navigation_intent(resolved, false);
2648
+
2649
+ if (!intent) {
2650
+ throw new Error(
2651
+ DEV
2652
+ ? `Cannot use \`${caller}\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
2653
+ : `${caller}: invalid URL`
2654
+ );
2655
+ }
2656
+
2657
+ return intent;
2658
+ }
2659
+
2660
+ /**
2661
+ * Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
2662
+ * (as they would be with a regular navigation) or preserved.
2256
2663
  *
2257
- * For external URLs, use `window.location = url` instead of calling `goto(url)`.
2664
+ * 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.
2258
2665
  *
2259
- * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.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.
2260
- * @param {Object} [opts] Options related to the navigation
2261
- * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
2262
- * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation
2263
- * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body
2264
- * @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://svelte.dev/docs/kit/load#rerunning-load-functions for more info on invalidation.
2265
- * @param {Array<string | URL | ((url: URL) => boolean)>} [opts.invalidate] Causes any load functions to re-run if they depend on one of the urls
2266
- * @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
2666
+ * `goto` is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
2667
+ * For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
2668
+ *
2669
+ * @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.
2670
+ * @param {GotoOptions} [opts] Options related to the navigation
2267
2671
  * @returns {Promise<void>}
2268
2672
  */
2269
- export function goto(url, opts = {}) {
2673
+ export async function goto(url, opts = {}) {
2270
2674
  if (!BROWSER) {
2271
2675
  throw new Error('Cannot call goto(...) on the server');
2272
2676
  }
2273
2677
 
2274
- url = new URL(resolve_url(url));
2678
+ if (DEV) {
2679
+ if ('replaceState' in opts && !warned_on_replace_state) {
2680
+ warned_on_replace_state = true;
2681
+ console.warn(
2682
+ `The \`goto(..., { replaceState: ${opts.replaceState} })\` option has been deprecated in favour of \`replace\``
2683
+ );
2684
+ }
2685
+
2686
+ if ('noScroll' in opts || 'keepFocus' in opts) {
2687
+ throw new Error(
2688
+ `The \`goto(..., { noScroll: true, keepFocus: true })\` options have been replaced by \`reset: false\``
2689
+ );
2690
+ }
2691
+ }
2692
+
2693
+ const replace = opts.replace ?? opts.replaceState ?? false;
2275
2694
 
2276
- if (url.origin !== origin) {
2277
- return Promise.reject(
2278
- new Error(
2279
- DEV
2280
- ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2281
- : 'goto: invalid URL'
2282
- )
2695
+ const intent = await resolve_intent(url, 'goto');
2696
+
2697
+ if (opts.shallow) {
2698
+ return update_state(
2699
+ intent,
2700
+ opts.state ?? {},
2701
+ {
2702
+ replace,
2703
+ persist_state: opts.persistState ?? false,
2704
+ reset: opts.reset ?? false
2705
+ },
2706
+ 'goto'
2707
+ );
2708
+ }
2709
+
2710
+ if (DEV && 'invalidateAll' in opts && !warned_on_invalidate_all) {
2711
+ warned_on_invalidate_all = true;
2712
+ console.warn(
2713
+ `The \`goto(..., { invalidateAll: ${opts.invalidateAll} })\` option has been deprecated in favour of \`refreshAll\``
2283
2714
  );
2284
2715
  }
2285
2716
 
2286
- return _goto(url, opts, 0);
2717
+ return _goto(
2718
+ intent.url,
2719
+ { ...opts, replace, refreshAll: opts.refreshAll ?? opts.invalidateAll },
2720
+ 0,
2721
+ {},
2722
+ intent
2723
+ );
2287
2724
  }
2288
2725
 
2289
2726
  /**
@@ -2302,16 +2739,17 @@ export function goto(url, opts = {}) {
2302
2739
  * invalidate((url) => url.pathname === '/path');
2303
2740
  * ```
2304
2741
  * @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
2742
+ * @param {boolean} [keepState] If `true`, the current `page.state` will be preserved. Otherwise, it will be reset to an empty object. `false` by default.
2305
2743
  * @returns {Promise<void>}
2306
2744
  */
2307
- export function invalidate(resource) {
2745
+ export function invalidate(resource, keepState = false) {
2308
2746
  if (!BROWSER) {
2309
2747
  throw new Error('Cannot call invalidate(...) on the server');
2310
2748
  }
2311
2749
 
2312
2750
  push_invalidated(resource);
2313
2751
 
2314
- return _invalidate();
2752
+ return _invalidate(!keepState);
2315
2753
  }
2316
2754
 
2317
2755
  /**
@@ -2328,6 +2766,10 @@ function push_invalidated(resource) {
2328
2766
 
2329
2767
  /**
2330
2768
  * Causes all `load` and `query` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
2769
+ *
2770
+ * Note that this resets `page.state` to an empty object. If you want to preserve `page.state` (for example when using [shallow routing](https://svelte.dev/docs/kit/shallow-routing)), use `refreshAll` instead.
2771
+ *
2772
+ * @deprecated Use [`refreshAll`](https://svelte.dev/docs/kit/$app-navigation#refreshAll) instead. Unlike `invalidateAll`, `refreshAll` does not reset `page.state`.
2331
2773
  * @returns {Promise<void>}
2332
2774
  */
2333
2775
  export function invalidateAll() {
@@ -2340,18 +2782,17 @@ export function invalidateAll() {
2340
2782
  }
2341
2783
 
2342
2784
  /**
2343
- * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run (unless disabled via the option argument).
2785
+ * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run.
2344
2786
  * Returns a `Promise` that resolves when the page is subsequently updated.
2345
- * @param {{ includeLoadFunctions?: boolean }} [options]
2346
2787
  * @returns {Promise<void>}
2347
2788
  */
2348
- export function refreshAll({ includeLoadFunctions = true } = {}) {
2789
+ export function refreshAll() {
2349
2790
  if (!BROWSER) {
2350
2791
  throw new Error('Cannot call refreshAll() on the server');
2351
2792
  }
2352
2793
 
2353
2794
  force_invalidation = true;
2354
- return _invalidate(includeLoadFunctions, false);
2795
+ return _invalidate(false);
2355
2796
  }
2356
2797
 
2357
2798
  /**
@@ -2364,7 +2805,7 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
2364
2805
  * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
2365
2806
  *
2366
2807
  * @param {string} href Page to preload
2367
- * @returns {Promise<{ type: 'loaded'; status: number; data: Record<string, any> } | { type: 'redirect'; location: string }>}
2808
+ * @returns {Promise<({ type: 'loaded'; data: Record<string, any> } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>}
2368
2809
  */
2369
2810
  export async function preloadData(href) {
2370
2811
  if (!BROWSER) {
@@ -2378,15 +2819,32 @@ export async function preloadData(href) {
2378
2819
  throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
2379
2820
  }
2380
2821
 
2381
- const result = await _preload_data(intent);
2822
+ /** @type {Awaited<ReturnType<typeof _preload_data>>} */
2823
+ let result;
2824
+
2825
+ try {
2826
+ result = await _preload_data(intent);
2827
+ } catch (error) {
2828
+ // `load_route` throws the handled error (an `App.Error` with a `status`)
2829
+ // when a preload fails, so surface it in the documented `{ type: 'error' }` shape
2830
+ const handled = /** @type {App.Error & { status?: number }} */ (error);
2831
+ return {
2832
+ type: 'error',
2833
+ status: handled?.status ?? 500,
2834
+ error: handled
2835
+ };
2836
+ }
2837
+
2382
2838
  if (result.type === 'redirect') {
2383
2839
  return {
2384
2840
  type: result.type,
2841
+ status: result.status,
2385
2842
  location: result.location
2386
2843
  };
2387
2844
  }
2388
2845
 
2389
2846
  const { status, data } = result.props.page ?? page;
2847
+
2390
2848
  return { type: result.type, status, data };
2391
2849
  }
2392
2850
 
@@ -2394,139 +2852,264 @@ export async function preloadData(href) {
2394
2852
  * Programmatically imports the code for routes that haven't yet been fetched.
2395
2853
  * Typically, you might call this to speed up subsequent navigation.
2396
2854
  *
2397
- * 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`).
2855
+ * Takes a route ID such as `/about` or `/blog/[slug]`. Unlike pathnames, route IDs
2856
+ * are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths).
2857
+ * If you have a pathname rather than a route ID, you can convert it with
2858
+ * [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`:
2859
+ *
2860
+ * ```js
2861
+ * import { match } from '$app/paths';
2862
+ * import { preloadCode } from '$app/navigation';
2863
+ *
2864
+ * const matched = await match('/blog/hello-world');
2865
+ * if (matched) await preloadCode(matched.id);
2866
+ * ```
2398
2867
  *
2399
2868
  * Unlike `preloadData`, this won't call `load` functions.
2400
2869
  * Returns a Promise that resolves when the modules have been imported.
2401
2870
  *
2402
- * @param {string} pathname
2871
+ * @param {import('$app/types').RouteId} id
2403
2872
  * @returns {Promise<void>}
2404
2873
  */
2405
- export async function preloadCode(pathname) {
2874
+ export async function preloadCode(id) {
2406
2875
  if (!BROWSER) {
2407
2876
  throw new Error('Cannot call preloadCode(...) on the server');
2408
2877
  }
2409
2878
 
2410
- const url = new URL(pathname, current.url);
2879
+ if (DEV && id[0] !== '/') {
2880
+ throw new Error(
2881
+ `argument passed to preloadCode must be a route ID (i.e. "/blog/[slug]" rather than "blog/[slug]")`
2882
+ );
2883
+ }
2411
2884
 
2412
- if (DEV) {
2413
- if (!pathname.startsWith('/')) {
2414
- throw new Error(
2415
- 'argument passed to preloadCode must be a pathname (i.e. "/about" rather than "http://example.com/about"'
2416
- );
2417
- }
2885
+ const route = __SVELTEKIT_CLIENT_ROUTING__
2886
+ ? routes.find((r) => r.id === id)
2887
+ : (route_id_cache.get(id) ?? (await load_route_by_id(id)));
2418
2888
 
2419
- if (!pathname.startsWith(base)) {
2420
- throw new Error(
2421
- `pathname passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")`
2889
+ if (route === ENDPOINT_ONLY) {
2890
+ if (DEV) {
2891
+ console.warn(
2892
+ `'${id}' has no \`+page\`, so there is no code to preload. If you meant to warm up an ` +
2893
+ `endpoint, request it with \`fetch\` instead.`
2422
2894
  );
2423
2895
  }
2424
2896
 
2425
- if (__SVELTEKIT_CLIENT_ROUTING__) {
2426
- const rerouted = await get_rerouted_url(url);
2427
- if (!rerouted || !routes.find((route) => route.exec(get_url_path(rerouted)))) {
2428
- throw new Error(`'${pathname}' did not match any routes`);
2897
+ return;
2898
+ }
2899
+
2900
+ if (!route) {
2901
+ if (DEV) {
2902
+ // warn rather than throw, since under client routing an endpoint-only route id is
2903
+ // indistinguishable from a typo — the client manifest only contains routes with a `+page`
2904
+ let message = `'${id}' did not match any route`;
2905
+
2906
+ if (__SVELTEKIT_CLIENT_ROUTING__) {
2907
+ message += ` (note that routes without a \`+page\` have no code to preload)`;
2908
+
2909
+ // the most common migration mistake is passing a pathname, which used to work
2910
+ const candidates = [id];
2911
+ if (base && id.startsWith(base)) candidates.push(id.slice(base.length) || '/');
2912
+
2913
+ if (candidates.some((path) => routes.some((r) => r.exec(path)))) {
2914
+ message += `. It does match as a pathname — use \`match(...)\` from \`$app/paths\` to convert a pathname into a route ID`;
2915
+ }
2429
2916
  }
2917
+
2918
+ console.warn(message);
2430
2919
  }
2920
+
2921
+ return;
2431
2922
  }
2432
2923
 
2433
- return _preload_code(url);
2924
+ await load_route_nodes(route);
2434
2925
  }
2435
2926
 
2436
2927
  /**
2437
- * 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).
2928
+ * Programmatically create a new history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2438
2929
  *
2930
+ * @deprecated Use `goto(url, { state, shallow: true })` instead.
2439
2931
  * @param {string | URL} url
2440
2932
  * @param {App.PageState} state
2441
- * @returns {void}
2933
+ * @returns {Promise<void>}
2442
2934
  */
2443
- export function pushState(url, state) {
2935
+ export async function pushState(url, state) {
2444
2936
  if (!BROWSER) {
2445
2937
  throw new Error('Cannot call pushState(...) on the server');
2446
2938
  }
2447
2939
 
2448
- if (DEV) {
2449
- if (!started) {
2450
- throw new Error('Cannot call pushState(...) before router is initialized');
2451
- }
2452
-
2453
- try {
2454
- // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2455
- devalue.stringify(state);
2456
- } catch (error) {
2457
- // @ts-expect-error
2458
- throw new Error(`Could not serialize state${error.path}`, { cause: error });
2459
- }
2940
+ if (DEV && !warned_on_push_state) {
2941
+ warned_on_push_state = true;
2942
+ console.warn(
2943
+ '`pushState(...)` is deprecated. Use `goto(url, { state, shallow: true })` instead.'
2944
+ );
2460
2945
  }
2461
2946
 
2462
- update_scroll_positions(current_history_index);
2463
-
2464
- const opts = {
2465
- [HISTORY_INDEX]: (current_history_index += 1),
2466
- [NAVIGATION_INDEX]: current_navigation_index,
2467
- [PAGE_URL_KEY]: page.url.href,
2468
- [STATES_KEY]: state
2469
- };
2470
-
2471
- history.pushState(opts, '', resolve_url(url));
2472
- has_navigated = true;
2473
-
2474
- page.state = state;
2475
- root.$set({
2476
- // we need to assign a new page object so that subscribers are correctly notified
2477
- page: untrack(() => clone_page(page))
2478
- });
2947
+ const intent = await resolve_intent(url, 'pushState');
2479
2948
 
2480
- clear_onward_history(current_history_index, current_navigation_index);
2949
+ await update_state(
2950
+ intent,
2951
+ state,
2952
+ { replace: false, persist_state: false, reset: false },
2953
+ 'pushState'
2954
+ );
2481
2955
  }
2482
2956
 
2483
2957
  /**
2484
- * 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).
2958
+ * Programmatically replace the current history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
2485
2959
  *
2960
+ * @deprecated Use `goto(url, { state, shallow: true, replace: true })` instead.
2486
2961
  * @param {string | URL} url
2487
2962
  * @param {App.PageState} state
2488
- * @returns {void}
2963
+ * @returns {Promise<void>}
2489
2964
  */
2490
- export function replaceState(url, state) {
2965
+ export async function replaceState(url, state) {
2491
2966
  if (!BROWSER) {
2492
2967
  throw new Error('Cannot call replaceState(...) on the server');
2493
2968
  }
2494
2969
 
2495
- if (DEV) {
2496
- if (!started) {
2497
- throw new Error('Cannot call replaceState(...) before router is initialized');
2498
- }
2970
+ if (DEV && !warned_on_replace_state_function) {
2971
+ warned_on_replace_state_function = true;
2972
+ console.warn(
2973
+ '`replaceState(...)` is deprecated. Use `goto(url, { state, shallow: true, replace: true })` instead.'
2974
+ );
2975
+ }
2499
2976
 
2500
- try {
2501
- // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
2502
- devalue.stringify(state);
2503
- } catch (error) {
2504
- // @ts-expect-error
2505
- throw new Error(`Could not serialize state${error.path}`, { cause: error });
2506
- }
2977
+ const intent = await resolve_intent(url, 'replaceState');
2978
+
2979
+ await update_state(
2980
+ intent,
2981
+ state,
2982
+ { replace: true, persist_state: false, reset: false },
2983
+ 'replaceState'
2984
+ );
2985
+ }
2986
+
2987
+ /**
2988
+ * @param {NavigationIntent} intent
2989
+ * @param {App.PageState} state
2990
+ * @param {{ replace: boolean; persist_state: boolean; reset: boolean; }} options
2991
+ * @param {'goto' | 'pushState' | 'replaceState'} caller
2992
+ */
2993
+ async function update_state(intent, state, { replace, persist_state, reset }, caller) {
2994
+ const url = intent.url;
2995
+ const previous_snapshot_registrations = current_registrations();
2996
+
2997
+ if (DEV && !started) {
2998
+ throw new Error(`Cannot call ${caller}(...) before router is initialized`);
2507
2999
  }
2508
3000
 
2509
- const opts = {
2510
- [HISTORY_INDEX]: current_history_index,
2511
- [NAVIGATION_INDEX]: current_navigation_index,
2512
- [PAGE_URL_KEY]: page.url.href,
2513
- [STATES_KEY]: state
3001
+ const nav =
3002
+ // For backwards compatibility we don't trigger navigation hooks etc for push/replaceState
3003
+ caller === 'goto' ? _before_navigate({ url, type: 'goto', intent, shallow: true }) : undefined;
3004
+
3005
+ if (!nav && caller === 'goto') return;
3006
+
3007
+ const nav_token = {};
3008
+
3009
+ if (nav) {
3010
+ navigation_token = invalidation_token = nav_token;
3011
+ is_navigating = true;
3012
+ navigating.current = nav.navigation;
3013
+ updating = true;
3014
+ }
3015
+
3016
+ if (replace) {
3017
+ delete_navigation_snapshot(current_history_index);
3018
+ } else {
3019
+ capture_scroll(current_history_index);
3020
+ capture_navigation_snapshot(current_history_index);
3021
+ }
3022
+ if (reset) current_reset_index += 1;
3023
+
3024
+ // as above, serialize-then-parse to prevent bugs
3025
+ const serialized_state = stringify(state);
3026
+ state = parse(serialized_state);
3027
+
3028
+ const entry = {
3029
+ [HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
3030
+ historyIndex: (current_history_index += replace ? 0 : 1),
3031
+ navigationIndex: current_navigation_index,
3032
+ pageUrl: page.url.href,
3033
+ state: serialized_state,
3034
+ persistState: persist_state,
3035
+ resetIndex: current_reset_index
3036
+ })
2514
3037
  };
2515
3038
 
2516
- history.replaceState(opts, '', resolve_url(url));
3039
+ const fn = replace ? history.replaceState : history.pushState;
3040
+ fn.call(history, entry, '', url);
3041
+ set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);
3042
+
3043
+ if (!replace) {
3044
+ has_navigated = true;
3045
+ clear_onward_history(current_history_index, current_navigation_index);
3046
+ }
3047
+
3048
+ if (nav) {
3049
+ const after_navigate = (
3050
+ await Promise.all(
3051
+ // eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
3052
+ Array.from(on_navigate_callbacks, (fn) => fn(/** @type {OnNavigate} */ (nav.navigation)))
3053
+ )
3054
+ ).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
3055
+
3056
+ if (after_navigate.length > 0) {
3057
+ function cleanup() {
3058
+ after_navigate.forEach((fn) => after_navigate_callbacks.delete(fn));
3059
+ }
3060
+
3061
+ after_navigate.push(cleanup);
3062
+ after_navigate.forEach((fn) => after_navigate_callbacks.add(fn));
3063
+ }
3064
+ }
3065
+
3066
+ blur_active_element(reset);
2517
3067
 
2518
3068
  page.state = state;
2519
- root.$set({
2520
- page: untrack(() => clone_page(page))
2521
- });
3069
+ page.shallow = {
3070
+ params: intent?.params ?? null,
3071
+ route: intent ? { id: intent.route.id } : null,
3072
+ url
3073
+ };
3074
+
3075
+ if (nav) {
3076
+ const { activeElement } = document;
3077
+
3078
+ await settled();
3079
+
3080
+ if (navigation_token !== nav_token) {
3081
+ // a new navigation happened while we were waiting for the DOM to update, so abort
3082
+ nav.reject(new Error('navigation aborted'));
3083
+ return;
3084
+ }
3085
+
3086
+ reset_scroll_and_focus(url, reset ? null : scroll_state(), reset, activeElement);
3087
+
3088
+ is_navigating = false;
3089
+ nav.fulfil(undefined);
3090
+
3091
+ if (nav.navigation.to) {
3092
+ nav.navigation.to.scroll = scroll_state();
3093
+ }
3094
+
3095
+ after_navigate_callbacks.forEach((fn) => fn(/** @type {AfterNavigate} */ (nav.navigation)));
3096
+ }
3097
+
3098
+ restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);
3099
+
3100
+ if (nav) {
3101
+ navigating.current = null;
3102
+ updating = false;
3103
+ }
2522
3104
  }
2523
3105
 
2524
3106
  /**
2525
- * This action updates the `form` property of the current page with the given data and updates `page.status`.
2526
- * In case of an error, it redirects to the nearest error page.
3107
+ * Updates the `form` property of the current page with the given data and updates `page.status`.
3108
+ * In case of an error, it renders the nearest error page. In case of a redirect, it navigates to
3109
+ * the redirect location.
2527
3110
  * @template {Record<string, unknown> | undefined} Success
2528
3111
  * @template {Record<string, unknown> | undefined} Failure
2529
- * @param {import('@sveltejs/kit').ActionResult<Success, Failure>} result
3112
+ * @param {ActionResult<Success, Failure>} result
2530
3113
  * @returns {Promise<void>}
2531
3114
  */
2532
3115
  export async function applyAction(result) {
@@ -2534,37 +3117,81 @@ export async function applyAction(result) {
2534
3117
  throw new Error('Cannot call applyAction(...) on the server');
2535
3118
  }
2536
3119
 
3120
+ if (result.type === 'redirect') {
3121
+ await _goto(result.location, { refreshAll: true });
3122
+ return;
3123
+ }
3124
+
2537
3125
  if (result.type === 'error') {
2538
- await set_nearest_error_page(result.error, result.status);
2539
- } else if (result.type === 'redirect') {
2540
- await _goto(result.location, { invalidateAll: true }, 0);
3126
+ await set_nearest_error_page(result.error);
2541
3127
  } else {
2542
3128
  page.form = result.data;
2543
3129
  page.status = result.status;
2544
3130
 
2545
3131
  /** @type {Record<string, any>} */
2546
- root.$set({
2547
- // this brings Svelte's view of the world in line with SvelteKit's
2548
- // after use:enhance reset the form....
2549
- form: null,
2550
- page: clone_page(page)
2551
- });
3132
+ // this brings Svelte's view of the world in line with SvelteKit's
3133
+ // after use:enhance reset the form....
3134
+ props.form = null;
2552
3135
 
2553
3136
  // ...so that setting the `form` prop takes effect and isn't ignored
2554
3137
  await tick();
2555
- root.$set({ form: result.data });
3138
+ props.form = result.data;
2556
3139
 
2557
3140
  if (result.type === 'success') {
2558
- reset_focus(page.url);
3141
+ reset_focus(/** @type {URL} */ (page.url));
3142
+ }
3143
+ }
3144
+ }
3145
+
3146
+ /**
3147
+ * Whether a submission should update the current page in place rather than navigating — either
3148
+ * because it lands where we already are, or because the result carries no location at all
3149
+ * (hand-constructed results, which have always applied in place).
3150
+ * @param {string | undefined} value
3151
+ */
3152
+ export function is_current_location(value) {
3153
+ if (value === undefined) return true;
3154
+
3155
+ const destination = resolve_url(value);
3156
+ const current = new URL(location.href);
3157
+ if (destination.pathname !== current.pathname) return false;
3158
+
3159
+ const keys = new Set([...destination.searchParams.keys(), ...current.searchParams.keys()]);
3160
+ for (const key of keys) {
3161
+ const destination_values = destination.searchParams.getAll(key).sort();
3162
+ const current_values = current.searchParams.getAll(key).sort();
3163
+
3164
+ if (
3165
+ destination_values.length !== current_values.length ||
3166
+ destination_values.some((value, i) => value !== current_values[i])
3167
+ ) {
3168
+ return false;
2559
3169
  }
2560
3170
  }
3171
+
3172
+ return true;
3173
+ }
3174
+
3175
+ /**
3176
+ * Navigate to where a form submission should land, rendering that page with the given result
3177
+ * — what the browser would do for a native submission.
3178
+ * @param {string} location
3179
+ * @param {ActionResult} result
3180
+ * @param {boolean} refresh_all
3181
+ * @returns {Promise<void>}
3182
+ */
3183
+ export function apply_action_navigation(location, result, refresh_all) {
3184
+ return _goto(location, {
3185
+ type: 'form',
3186
+ refreshAll: refresh_all,
3187
+ action_result: result
3188
+ });
2561
3189
  }
2562
3190
 
2563
3191
  /**
2564
3192
  * @param {App.Error} error
2565
- * @param {number} status
2566
3193
  */
2567
- export async function set_nearest_error_page(error, status = 500) {
3194
+ export async function set_nearest_error_page(error) {
2568
3195
  const url = new URL(location.href);
2569
3196
 
2570
3197
  const { branch, route } = current;
@@ -2576,7 +3203,6 @@ export async function set_nearest_error_page(error, status = 500) {
2576
3203
  url,
2577
3204
  params: current.params,
2578
3205
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
2579
- status,
2580
3206
  error,
2581
3207
  // do not set errors, we haven't changed the page so the previous ones are still current
2582
3208
  route
@@ -2584,8 +3210,7 @@ export async function set_nearest_error_page(error, status = 500) {
2584
3210
 
2585
3211
  current = { ...navigation_result.state, nav: current.nav };
2586
3212
 
2587
- root.$set(navigation_result.props);
2588
- update(navigation_result.props.page);
3213
+ apply_navigation_result(navigation_result);
2589
3214
 
2590
3215
  void tick().then(() => reset_focus(current.url));
2591
3216
  }
@@ -2608,7 +3233,7 @@ function _start_router() {
2608
3233
 
2609
3234
  // If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
2610
3235
  // it's due to an external or full-page-reload link, for which we don't want to call the hook again.
2611
- /** @type {import('@sveltejs/kit').BeforeNavigate} */
3236
+ /** @type {BeforeNavigate} */
2612
3237
  const navigation = {
2613
3238
  ...nav.navigation,
2614
3239
  cancel: () => {
@@ -2627,13 +3252,19 @@ function _start_router() {
2627
3252
  history.scrollRestoration = 'auto';
2628
3253
  }
2629
3254
  });
2630
-
2631
3255
  addEventListener('visibilitychange', () => {
2632
3256
  if (document.visibilityState === 'hidden') {
2633
3257
  persist_state();
3258
+ } else {
3259
+ // the tab just became visible — a good time to check for a new deployment
3260
+ void updated.check();
2634
3261
  }
2635
3262
  });
2636
3263
 
3264
+ addEventListener('focus', () => {
3265
+ void updated.check();
3266
+ });
3267
+
2637
3268
  // @ts-expect-error this isn't supported everywhere yet
2638
3269
  if (!navigator.connection?.saveData) {
2639
3270
  setup_preload();
@@ -2724,17 +3355,21 @@ function _start_router() {
2724
3355
  return;
2725
3356
  }
2726
3357
  // set this flag to distinguish between navigations triggered by
2727
- // clicking a hash link and those triggered by popstate
2728
- hash_navigating = true;
3358
+ // clicking a hash link and those triggered by popstate. We gotta retrieve
3359
+ // history metadata here because the hashchange event will occur after history.state was updated
3360
+ hash_navigating = {
3361
+ .../** @type {HistoryMetadata} */ (get_history_metadata()),
3362
+ resetIndex: current_reset_index + (options.reset ? 1 : 0)
3363
+ };
2729
3364
 
2730
- update_scroll_positions(current_history_index);
3365
+ capture_scroll(current_history_index);
2731
3366
 
2732
3367
  update_url(url);
2733
3368
 
2734
3369
  if (!options.replace_state) return;
2735
3370
 
2736
3371
  // hashchange event shouldn't occur if the router is replacing state.
2737
- hash_navigating = false;
3372
+ hash_navigating = null;
2738
3373
  }
2739
3374
 
2740
3375
  event.preventDefault();
@@ -2749,12 +3384,13 @@ function _start_router() {
2749
3384
  setTimeout(fulfil, 100); // fallback for edge case where rAF doesn't fire because e.g. tab was backgrounded
2750
3385
  });
2751
3386
 
2752
- await navigate({
3387
+ const changed = url.href !== location.href;
3388
+
3389
+ await _goto(url, {
2753
3390
  type: 'link',
2754
- url,
2755
- keepfocus: options.keepfocus,
2756
- noscroll: options.noscroll,
2757
- replace_state: options.replace_state ?? url.href === location.href,
3391
+ reset: options.reset,
3392
+ replace: options.replace_state ?? !changed,
3393
+ refreshAll: !changed,
2758
3394
  event
2759
3395
  });
2760
3396
  });
@@ -2799,8 +3435,7 @@ function _start_router() {
2799
3435
  void navigate({
2800
3436
  type: 'form',
2801
3437
  url,
2802
- keepfocus: options.keepfocus,
2803
- noscroll: options.noscroll,
3438
+ reset: options.reset,
2804
3439
  replace_state: options.replace_state ?? url.href === location.href,
2805
3440
  event
2806
3441
  });
@@ -2809,58 +3444,89 @@ function _start_router() {
2809
3444
  addEventListener('popstate', async (event) => {
2810
3445
  if (resetting_focus) return;
2811
3446
 
2812
- if (event.state?.[HISTORY_INDEX]) {
2813
- const history_index = event.state[HISTORY_INDEX];
2814
- token = {};
3447
+ const history_metadata = get_history_metadata(event.state);
3448
+
3449
+ if (history_metadata?.historyIndex) {
3450
+ const history_index = history_metadata.historyIndex;
3451
+ const source_info = history_info[current_history_index];
3452
+ navigation_token = invalidation_token = {};
2815
3453
 
2816
3454
  // if a popstate-driven navigation is cancelled, we need to counteract it
2817
3455
  // with history.go, which means we end up back here, hence this check
2818
3456
  if (history_index === current_history_index) return;
2819
3457
 
2820
- const scroll = scroll_positions[history_index];
2821
- const state = event.state[STATES_KEY] ?? {};
2822
- const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
2823
- const navigation_index = event.state[NAVIGATION_INDEX];
2824
- const is_hash_change = current.url ? strip_hash(location) === strip_hash(current.url) : false;
3458
+ const delta = history_index - current_history_index;
3459
+ const reset_index = history_metadata.resetIndex;
3460
+ const reset = reset_index !== (source_info?.resetIndex ?? current_reset_index);
3461
+ const scroll = history_info[history_index]?.scroll;
3462
+ const state = parse(history_metadata.state);
3463
+ const url = new URL(history_metadata.pageUrl ?? location.href);
3464
+ const navigation_index = history_metadata.navigationIndex;
3465
+ const is_hash_change =
3466
+ current.url && (location.href + current.url.href).includes('#') // check if even has a hash
3467
+ ? strip_hash(location) === strip_hash(current.url)
3468
+ : false;
2825
3469
  const shallow =
2826
- navigation_index === current_navigation_index && (has_navigated || is_hash_change);
3470
+ navigation_index === current_navigation_index &&
3471
+ ((has_navigated &&
3472
+ (history_metadata.pageUrl === undefined || history_metadata.pageUrl === location.href)) ||
3473
+ is_hash_change);
3474
+ const shallow_url = history_metadata.pageUrl ? new URL(location.href) : null;
3475
+ const shallow_intent = shallow_url
3476
+ ? await get_navigation_intent(shallow_url, false)
3477
+ : undefined;
3478
+ const shallow_target = shallow_url
3479
+ ? {
3480
+ params: shallow_intent?.params ?? null,
3481
+ route: shallow_intent ? { id: shallow_intent.route.id } : null,
3482
+ url: shallow_url
3483
+ }
3484
+ : null;
2827
3485
 
2828
3486
  if (shallow) {
2829
3487
  // We don't need to navigate, we just need to update scroll and/or state.
2830
3488
  // This happens with hash links and `pushState`/`replaceState`. The
2831
3489
  // exception is if we haven't navigated yet, since we could have
2832
3490
  // got here after a modal navigation then a reload
3491
+
3492
+ blur_active_element(reset);
3493
+
2833
3494
  if (state !== page.state) {
2834
3495
  page.state = state;
2835
3496
  }
2836
3497
 
2837
- update_url(url);
3498
+ page.shallow = shallow_target;
2838
3499
 
2839
- scroll_positions[current_history_index] = scroll_state();
2840
- if (scroll) scrollTo(scroll.x, scroll.y);
3500
+ update_url(url);
2841
3501
 
3502
+ capture_scroll(current_history_index);
3503
+ capture_navigation_snapshot(current_history_index);
2842
3504
  current_history_index = history_index;
3505
+ current_reset_index = reset_index;
3506
+ if (reset && scroll) scrollTo(scroll.x, scroll.y);
3507
+ restore_navigation_snapshot(current_history_index, current_registrations());
2843
3508
  return;
2844
3509
  }
2845
3510
 
2846
- const delta = history_index - current_history_index;
2847
-
2848
3511
  await navigate({
2849
3512
  type: 'popstate',
2850
3513
  url,
3514
+ reset,
2851
3515
  popped: {
2852
3516
  state,
2853
3517
  scroll,
2854
- delta
3518
+ delta,
3519
+ shallow: shallow_target
2855
3520
  },
2856
3521
  accept: () => {
2857
3522
  current_history_index = history_index;
2858
3523
  current_navigation_index = navigation_index;
3524
+ current_reset_index = reset_index;
2859
3525
  },
2860
3526
  block: () => {
2861
3527
  history.go(-delta);
2862
3528
  },
2863
- nav_token: token,
3529
+ nav_token: navigation_token,
2864
3530
  event
2865
3531
  });
2866
3532
  } else {
@@ -2884,16 +3550,22 @@ function _start_router() {
2884
3550
  // if the hashchange happened as a result of clicking on a link,
2885
3551
  // we need to update history, otherwise we have to leave it alone
2886
3552
  if (hash_navigating) {
2887
- hash_navigating = false;
3553
+ const history_metadata = hash_navigating;
3554
+ hash_navigating = null;
3555
+ current_reset_index = history_metadata.resetIndex;
2888
3556
  history.replaceState(
2889
3557
  {
2890
3558
  ...history.state,
2891
- [HISTORY_INDEX]: ++current_history_index,
2892
- [NAVIGATION_INDEX]: current_navigation_index
3559
+ [HISTORY_METADATA_KEY]: {
3560
+ ...history_metadata,
3561
+ historyIndex: ++current_history_index,
3562
+ navigationIndex: current_navigation_index
3563
+ }
2893
3564
  },
2894
3565
  '',
2895
3566
  location.href
2896
3567
  );
3568
+ set_history_options(current_history_index, history_metadata);
2897
3569
  }
2898
3570
  });
2899
3571
 
@@ -2912,7 +3584,7 @@ function _start_router() {
2912
3584
  // the navigation away from it was successful.
2913
3585
  // Info about bfcache here: https://web.dev/bfcache
2914
3586
  if (event.persisted) {
2915
- stores.navigating.set((navigating.current = null));
3587
+ navigating.current = null;
2916
3588
  }
2917
3589
  });
2918
3590
 
@@ -2921,18 +3593,17 @@ function _start_router() {
2921
3593
  */
2922
3594
  function update_url(url) {
2923
3595
  current.url = page.url = url;
2924
- stores.page.set(clone_page(page));
2925
- stores.page.notify();
2926
3596
  }
2927
3597
  }
2928
3598
 
2929
3599
  /**
2930
3600
  * @param {HTMLElement} target
2931
3601
  * @param {import('./types.js').HydrateOptions} opts
3602
+ * @returns {Promise<void>}
2932
3603
  */
2933
3604
  async function _hydrate(
2934
3605
  target,
2935
- { status = 200, error, node_ids, params, route, server_route, data: server_data_nodes, form }
3606
+ { status, error, node_ids, params, route, server_route, data: server_data_nodes, form }
2936
3607
  ) {
2937
3608
  hydrated = true;
2938
3609
 
@@ -2952,7 +3623,7 @@ async function _hydrate(
2952
3623
  } else {
2953
3624
  // undefined in case of 404
2954
3625
  if (server_route) {
2955
- parsed_route = route = parse_server_route(server_route, app.nodes);
3626
+ parsed_route = route = parse_and_cache_server_route(server_route);
2956
3627
  } else {
2957
3628
  route = { id: null };
2958
3629
  params = {};
@@ -2961,7 +3632,7 @@ async function _hydrate(
2961
3632
 
2962
3633
  /** @type {import('./types.js').NavigationFinished | undefined} */
2963
3634
  let result;
2964
- let hydrate = true;
3635
+ let should_hydrate = true;
2965
3636
 
2966
3637
  try {
2967
3638
  const branch_promises = node_ids.map(async (n, i) => {
@@ -3015,28 +3686,31 @@ async function _hydrate(
3015
3686
  if (error instanceof Redirect) {
3016
3687
  // this is a real edge case — `load` would need to return
3017
3688
  // a redirect but only in the browser
3018
- await native_navigation(new URL(error.location, location.href));
3019
- return;
3689
+ return await native_navigation(new URL(error.location, location.href));
3020
3690
  }
3021
3691
 
3692
+ const handled_error = await handle_error(error, { url, params, route });
3693
+
3022
3694
  result = await load_root_error_page({
3023
- status: get_status(error),
3024
- error: await handle_error(error, { url, params, route }),
3695
+ error: handled_error,
3025
3696
  url,
3026
3697
  route
3027
3698
  });
3028
3699
 
3029
3700
  target.textContent = '';
3030
- hydrate = false;
3031
- } finally {
3032
- query_responses = {};
3701
+ should_hydrate = false;
3033
3702
  }
3034
3703
 
3704
+ // Exit early when we encounter a redirect while loading the root error page.
3705
+ // In this case, `initialize` will be called later on
3706
+ if (!result) return;
3707
+
3035
3708
  if (result.props.page) {
3036
- result.props.page.state = {};
3709
+ const history_metadata = get_history_metadata();
3710
+ result.props.page.state = history_metadata?.persistState ? parse(history_metadata.state) : {};
3037
3711
  }
3038
3712
 
3039
- await initialize(result, target, hydrate);
3713
+ await initialize(result, target, should_hydrate);
3040
3714
  }
3041
3715
 
3042
3716
  /**
@@ -3059,21 +3733,22 @@ async function load_data(url, invalid) {
3059
3733
  const fetcher = DEV ? dev_fetch : window.fetch;
3060
3734
  const res = await fetcher(data_url.href, {});
3061
3735
 
3736
+ // detect new deployments from the response header
3737
+ notify_version(res.headers.get('x-sveltekit-version'));
3738
+
3062
3739
  if (!res.ok) {
3063
- // error message is a JSON-stringified string which devalue can't handle at the top level
3064
- // turn it into a HttpError to not call handleError on the client again (was already handled on the server)
3065
3740
  // if `__data.json` doesn't exist or the server has an internal error,
3066
3741
  // avoid parsing the HTML error page as a JSON
3067
- /** @type {string | undefined} */
3068
- let message;
3742
+ /** @type {App.Error} */
3743
+ let error = { status: res.status, message: 'Internal Error' };
3744
+
3069
3745
  if (res.headers.get('content-type')?.includes('application/json')) {
3070
- message = await res.json();
3746
+ error = { status: res.status, ...(await res.json()) };
3071
3747
  } else if (res.status === 404) {
3072
- message = 'Not Found';
3073
- } else if (res.status === 500) {
3074
- message = 'Internal Error';
3748
+ error.message = 'Not Found';
3075
3749
  }
3076
- throw new HttpError(res.status, message);
3750
+
3751
+ throw new HandledHttpError(error);
3077
3752
  }
3078
3753
 
3079
3754
  return new Promise((resolve, reject) => {
@@ -3176,8 +3851,8 @@ function reset_focus(url, scroll = true) {
3176
3851
 
3177
3852
  // Mimic the browsers' behaviour and set the sequential focus navigation
3178
3853
  // starting point to the fragment identifier.
3179
- const id = get_id(url);
3180
- if (id && document.getElementById(id)) {
3854
+ const element = get_hash_element(url);
3855
+ if (element) {
3181
3856
  const { x, y } = scroll_state();
3182
3857
 
3183
3858
  // `element.focus()` doesn't work on Safari and Firefox Ubuntu so we need
@@ -3186,7 +3861,7 @@ function reset_focus(url, scroll = true) {
3186
3861
  const history_state = history.state;
3187
3862
 
3188
3863
  resetting_focus = true;
3189
- location.replace(new URL(`#${id}`, location.href));
3864
+ location.replace(new URL(`#${element.id}`, location.href));
3190
3865
 
3191
3866
  // Firefox has a bug that sets the history state to `null` so we need to
3192
3867
  // restore it after. See https://bugzilla.mozilla.org/show_bug.cgi?id=1199924
@@ -3208,9 +3883,6 @@ function reset_focus(url, scroll = true) {
3208
3883
  const tabindex = root.getAttribute('tabindex');
3209
3884
 
3210
3885
  root.tabIndex = -1;
3211
- // TODO: remove this when we switch to TypeScript 6
3212
- // @ts-ignore options.focusVisible is only typed in TypeScript 6
3213
- // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#browser_compatibility
3214
3886
  root.focus({ preventScroll: true, focusVisible: false });
3215
3887
 
3216
3888
  // restore `tabindex` as to prevent `root` from stealing input from elements
@@ -3263,14 +3935,24 @@ function reset_focus(url, scroll = true) {
3263
3935
  }
3264
3936
 
3265
3937
  /**
3266
- * @template {import('@sveltejs/kit').NavigationType} T
3938
+ * @template {NavigationType} T
3267
3939
  * @param {import('./types.js').NavigationState} current
3268
3940
  * @param {import('./types.js').NavigationIntent | undefined} intent
3269
3941
  * @param {URL | null} url
3270
3942
  * @param {T} type
3271
3943
  * @param {{ x: number, y: number } | null} [target_scroll] The scroll position for the target (for popstate navigations)
3944
+ * @param {boolean} [shallow]
3945
+ * @param {{ params: Record<string, string> | null; route: { id: string } | null; url: URL }} [target]
3272
3946
  */
3273
- function create_navigation(current, intent, url, type, target_scroll = null) {
3947
+ function create_navigation(
3948
+ current,
3949
+ intent,
3950
+ url,
3951
+ type,
3952
+ target_scroll = null,
3953
+ shallow = false,
3954
+ target
3955
+ ) {
3274
3956
  /** @type {(value: any) => void} */
3275
3957
  let fulfil;
3276
3958
 
@@ -3285,7 +3967,7 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
3285
3967
  // Handle any errors off-chain so that it doesn't show up as an unhandled rejection
3286
3968
  complete.catch(noop);
3287
3969
 
3288
- /** @type {(import('@sveltejs/kit').Navigation | import('@sveltejs/kit').AfterNavigate) & { type: T }} */
3970
+ /** @type {(Navigation | AfterNavigate) & { type: T }} */
3289
3971
  const navigation = /** @type {any} */ ({
3290
3972
  from: {
3291
3973
  params: current.params,
@@ -3294,13 +3976,14 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
3294
3976
  scroll: scroll_state()
3295
3977
  },
3296
3978
  to: url && {
3297
- params: intent?.params ?? null,
3298
- route: { id: intent?.route?.id ?? null },
3299
- url,
3979
+ params: target ? target.params : (intent?.params ?? null),
3980
+ route: { id: target ? (target.route?.id ?? null) : (intent?.route?.id ?? null) },
3981
+ url: target?.url ?? url,
3300
3982
  scroll: target_scroll
3301
3983
  },
3302
- willUnload: !intent,
3984
+ willUnload: !shallow && !intent,
3303
3985
  type,
3986
+ shallow,
3304
3987
  complete
3305
3988
  });
3306
3989
 
@@ -3313,28 +3996,6 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
3313
3996
  };
3314
3997
  }
3315
3998
 
3316
- /**
3317
- * TODO: remove this in 3.0 when the page store is also removed
3318
- *
3319
- * We need to assign a new page object so that subscribers are correctly notified.
3320
- * However, spreading `{ ...page }` returns an empty object so we manually
3321
- * assign to each property instead.
3322
- *
3323
- * @param {import('@sveltejs/kit').Page} page
3324
- */
3325
- function clone_page(page) {
3326
- return {
3327
- data: page.data,
3328
- error: page.error,
3329
- form: page.form,
3330
- params: page.params,
3331
- route: page.route,
3332
- state: page.state,
3333
- status: page.status,
3334
- url: page.url
3335
- };
3336
- }
3337
-
3338
3999
  /**
3339
4000
  * @param {URL} url
3340
4001
  * @returns {URL}
@@ -3363,6 +4024,15 @@ function get_id(url) {
3363
4024
  return decodeURIComponent(id);
3364
4025
  }
3365
4026
 
4027
+ /**
4028
+ * @param {URL} url
4029
+ * @returns {Element | null}
4030
+ */
4031
+ function get_hash_element(url) {
4032
+ const id = get_id(url);
4033
+ return id ? document.getElementById(id) : null;
4034
+ }
4035
+
3366
4036
  if (DEV) {
3367
4037
  // Nasty hack to silence harmless warnings the user can do nothing about
3368
4038
  const console_warn = console.warn;
@@ -3386,3 +4056,21 @@ if (DEV) {
3386
4056
  });
3387
4057
  }
3388
4058
  }
4059
+
4060
+ /**
4061
+ * @param {NavigationFinished} result
4062
+ */
4063
+ function apply_navigation_result(result) {
4064
+ Object.assign(page, result.props.page);
4065
+
4066
+ props.tree.data = result.props.tree.data;
4067
+ props.tree.child = result.props.tree.child;
4068
+
4069
+ if ('form' in result.props) {
4070
+ props.form = result.props.form;
4071
+ }
4072
+
4073
+ if ('error' in result.props) {
4074
+ props.error = result.props.error;
4075
+ }
4076
+ }