@sveltejs/kit 3.0.0-next.4 → 3.0.0-next.7

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 (91) hide show
  1. package/package.json +23 -18
  2. package/src/core/adapt/builder.js +29 -43
  3. package/src/core/adapt/index.js +1 -4
  4. package/src/core/config/index.js +135 -71
  5. package/src/core/config/options.js +291 -244
  6. package/src/core/config/types.d.ts +1 -1
  7. package/src/core/env.js +120 -5
  8. package/src/core/generate_manifest/index.js +12 -15
  9. package/src/core/postbuild/analyse.js +8 -12
  10. package/src/core/postbuild/crawl.js +22 -6
  11. package/src/core/postbuild/prerender.js +40 -23
  12. package/src/core/sync/create_manifest_data/index.js +29 -49
  13. package/src/core/sync/write_client_manifest.js +14 -14
  14. package/src/core/sync/write_non_ambient.js +10 -7
  15. package/src/core/sync/write_root.js +9 -30
  16. package/src/core/sync/write_server.js +13 -11
  17. package/src/core/sync/write_tsconfig.js +2 -4
  18. package/src/core/sync/write_types/index.js +11 -10
  19. package/src/exports/index.js +34 -12
  20. package/src/exports/internal/client.js +5 -0
  21. package/src/exports/internal/env.js +1 -1
  22. package/src/exports/internal/index.js +1 -90
  23. package/src/exports/internal/{event.js → server/event.js} +1 -2
  24. package/src/exports/internal/server/index.js +33 -0
  25. package/src/exports/internal/shared.js +89 -0
  26. package/src/exports/node/index.js +58 -7
  27. package/src/exports/params.js +63 -0
  28. package/src/exports/public.d.ts +299 -145
  29. package/src/exports/url.js +84 -0
  30. package/src/exports/vite/build/build_server.js +6 -1
  31. package/src/exports/vite/dev/index.js +38 -37
  32. package/src/exports/vite/index.js +498 -353
  33. package/src/exports/vite/preview/index.js +16 -8
  34. package/src/exports/vite/utils.js +7 -11
  35. package/src/runtime/app/env/types.d.ts +1 -1
  36. package/src/runtime/app/forms.js +22 -5
  37. package/src/runtime/app/paths/client.js +4 -10
  38. package/src/runtime/app/paths/public.d.ts +0 -28
  39. package/src/runtime/app/paths/server.js +8 -4
  40. package/src/runtime/app/server/remote/form.js +10 -3
  41. package/src/runtime/app/server/remote/query.js +9 -18
  42. package/src/runtime/app/server/remote/requested.js +8 -4
  43. package/src/runtime/app/server/remote/shared.js +5 -7
  44. package/src/runtime/app/state/client.js +1 -2
  45. package/src/runtime/app/stores.js +13 -76
  46. package/src/runtime/client/client.js +205 -167
  47. package/src/runtime/client/fetcher.js +3 -2
  48. package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
  49. package/src/runtime/client/remote-functions/form.svelte.js +122 -31
  50. package/src/runtime/client/remote-functions/prerender.svelte.js +17 -4
  51. package/src/runtime/client/remote-functions/query/index.js +3 -2
  52. package/src/runtime/client/remote-functions/query/instance.svelte.js +27 -11
  53. package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
  54. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +42 -15
  55. package/src/runtime/client/remote-functions/shared.svelte.js +18 -9
  56. package/src/runtime/client/state.svelte.js +66 -49
  57. package/src/runtime/client/types.d.ts +10 -2
  58. package/src/runtime/client/utils.js +0 -96
  59. package/src/runtime/form-utils.js +99 -18
  60. package/src/runtime/server/cookie.js +22 -33
  61. package/src/runtime/server/csrf.js +65 -0
  62. package/src/runtime/server/data/index.js +7 -7
  63. package/src/runtime/server/env_module.js +0 -5
  64. package/src/runtime/server/index.js +2 -2
  65. package/src/runtime/server/page/actions.js +41 -26
  66. package/src/runtime/server/page/index.js +2 -1
  67. package/src/runtime/server/page/load_data.js +1 -1
  68. package/src/runtime/server/page/render.js +36 -47
  69. package/src/runtime/server/page/respond_with_error.js +7 -8
  70. package/src/runtime/server/page/server_routing.js +13 -9
  71. package/src/runtime/server/remote.js +85 -39
  72. package/src/runtime/server/respond.js +92 -58
  73. package/src/runtime/server/utils.js +7 -7
  74. package/src/runtime/telemetry/otel.js +1 -1
  75. package/src/types/ambient.d.ts +10 -5
  76. package/src/types/global-private.d.ts +4 -4
  77. package/src/types/internal.d.ts +17 -20
  78. package/src/types/private.d.ts +33 -1
  79. package/src/types/synthetic/$lib.md +1 -1
  80. package/src/utils/error.js +12 -4
  81. package/src/utils/mime.js +9 -0
  82. package/src/utils/params.js +66 -0
  83. package/src/utils/routing.js +84 -38
  84. package/src/utils/shared-iterator.js +5 -0
  85. package/src/utils/streaming.js +14 -4
  86. package/src/utils/url.js +20 -2
  87. package/src/version.js +1 -1
  88. package/types/index.d.ts +434 -269
  89. package/types/index.d.ts.map +19 -15
  90. package/src/exports/internal/server.js +0 -22
  91. /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
@@ -1,20 +1,12 @@
1
1
  /** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */
2
+ /** @import { NavigationIntent } from './types.js' */
2
3
  /** @import { CacheEntry } from './remote-functions/cache.svelte.js' */
3
4
  /** @import { Query } from './remote-functions/query/instance.svelte.js' */
4
5
  /** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */
5
6
  import { BROWSER, DEV } from 'esm-env';
6
7
  import * as svelte from 'svelte';
7
8
  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';
9
+ import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js';
18
10
  import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
19
11
  import { parse, parse_server_route } from './parse.js';
20
12
  import * as storage from './session-storage.js';
@@ -26,11 +18,9 @@ import {
26
18
  is_external_url,
27
19
  origin,
28
20
  scroll_state,
29
- notifiable_store,
30
- create_updated_store,
31
21
  load_css
32
22
  } from './utils.js';
33
- import { base } from '$app/paths';
23
+ import { base } from '$app/paths/internal/client';
34
24
  import * as devalue from 'devalue';
35
25
  import {
36
26
  HISTORY_INDEX,
@@ -52,8 +42,7 @@ import {
52
42
  validate_load_response
53
43
  } from '../shared.js';
54
44
  import { get_message, get_status } from '../../utils/error.js';
55
- import { writable } from 'svelte/store';
56
- import { page, update, navigating } from './state.svelte.js';
45
+ import { page, update, navigating, updated } from './state.svelte.js';
57
46
  import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
58
47
  import { noop_span } from '../telemetry/noop.js';
59
48
  import { read_ndjson } from './ndjson.js';
@@ -90,8 +79,6 @@ const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
90
79
  if (DEV && BROWSER) {
91
80
  let warned = false;
92
81
 
93
- const current_module_url = import.meta.url.split('?')[0]; // remove query params that vite adds to the URL when it is loaded from node_modules
94
-
95
82
  const warn = () => {
96
83
  if (warned) return;
97
84
 
@@ -100,9 +87,13 @@ if (DEV && BROWSER) {
100
87
  let stack = new Error().stack?.split('\n');
101
88
  if (!stack) return;
102
89
  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;
90
+
91
+ // skip over `warn` and the place where `warn` was called
92
+ const frame = stack[2];
93
+
94
+ // ignore calls that happen inside dependencies, including SvelteKit.
95
+ // `frame` can be falsy if we came from an anonymous function
96
+ if (frame?.includes('node_modules')) return;
106
97
 
107
98
  warned = true;
108
99
 
@@ -124,15 +115,6 @@ if (DEV && BROWSER) {
124
115
  };
125
116
  }
126
117
 
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
118
  /** @param {number} index */
137
119
  function update_scroll_positions(index) {
138
120
  scroll_positions[index] = scroll_state();
@@ -163,7 +145,8 @@ function clear_onward_history(current_history_index, current_navigation_index) {
163
145
  * Returns a `Promise` that never resolves (to prevent any
164
146
  * subsequent work, e.g. history manipulation, from happening)
165
147
  * @param {URL} url
166
- * @param {boolean} [replace] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
148
+ * @param {boolean} [replace] if `true`, will replace the current `history` entry rather than creating a new one with `pushState`
149
+ * @returns {Promise<any>} a promise that never resolves
167
150
  */
168
151
  function native_navigation(url, replace = false) {
169
152
  if (replace) {
@@ -187,7 +170,7 @@ async function update_service_worker() {
187
170
  }
188
171
  }
189
172
 
190
- /** @type {import('types').CSRRoute[]} All routes of the app. Only available when kit.router.resolution=client */
173
+ /** @type {import('types').CSRRoute[]} All routes of the app. Only available when router.resolution=client */
191
174
  let routes;
192
175
  /** @type {import('types').CSRPageNodeLoader} */
193
176
  let default_layout_loader;
@@ -292,8 +275,17 @@ let current_history_index;
292
275
  /** @type {number} */
293
276
  let current_navigation_index;
294
277
 
295
- /** @type {{}} */
296
- let token;
278
+ /** @type {{}} Token for the latest navigation. Updated on new navigations */
279
+ let navigation_token;
280
+
281
+ /**
282
+ * @type {{}}
283
+ * The latest invalidate(All) token. Superseeded by both later invalidate(All)s and navigations.
284
+ * This is separate to navigation_token because an invalidate(All) might be triggered while a navigation
285
+ * is in progress, and we want to be able to finish this navigation (unless the invalidation finishes before
286
+ * it and redirects, in which case we will do the redirect triggered by the invalidation).
287
+ */
288
+ let invalidation_token;
297
289
 
298
290
  /**
299
291
  * A set of tokens which are associated to current preloads.
@@ -301,6 +293,7 @@ let token;
301
293
  * If a preload token is in the set and the preload errors, the error
302
294
  * handling logic (for example reloading) is skipped.
303
295
  */
296
+ /** @type {Set<{}>} */
304
297
  const preload_tokens = new Set();
305
298
 
306
299
  /** @type {Promise<void> | null} */
@@ -418,7 +411,9 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
418
411
  if (!pending_invalidate) return;
419
412
  pending_invalidate = null;
420
413
 
421
- const nav_token = (token = {});
414
+ const token = (invalidation_token = {});
415
+ const nav_token = navigation_token;
416
+ const navigating = is_navigating;
422
417
  const intent = await get_navigation_intent(current.url, true);
423
418
 
424
419
  // Clear preload, it might be affected by the invalidation.
@@ -450,17 +445,25 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
450
445
  if (include_load_functions) {
451
446
  const prev_state = page.state;
452
447
  const navigation_result = intent && (await load_route(intent));
453
- if (!navigation_result || nav_token !== token) return;
448
+ if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
449
+ return;
450
+ }
454
451
 
455
452
  if (navigation_result.type === 'redirect') {
456
453
  return _goto(
457
454
  new URL(navigation_result.location, current.url).href,
458
455
  { replaceState: true },
459
456
  1,
460
- nav_token
457
+ token
461
458
  );
462
459
  }
463
460
 
461
+ // A navigation started before the invalidation and ended before it finished. The invalidation did not redirect,
462
+ // hence it likely contains outdated data now, so we ignore it.
463
+ if (navigating && !is_navigating) {
464
+ return;
465
+ }
466
+
464
467
  // This is a bit hacky but allows us not having to pass that boolean around, making things harder to reason about
465
468
  if (!reset_page_state) {
466
469
  navigation_result.props.page.state = prev_state;
@@ -527,8 +530,10 @@ function persist_state() {
527
530
  * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
528
531
  * @param {number} redirect_count
529
532
  * @param {{}} [nav_token]
533
+ * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
534
+ * @returns {Promise<void>}
530
535
  */
531
- export async function _goto(url, options, redirect_count, nav_token) {
536
+ export async function _goto(url, options, redirect_count, nav_token, intent) {
532
537
  /** @type {Set<string>} */
533
538
  let query_keys;
534
539
  /** @type {Set<string>} */
@@ -549,6 +554,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
549
554
  state: options.state,
550
555
  redirect_count,
551
556
  nav_token,
557
+ intent,
552
558
  accept: () => {
553
559
  if (options.invalidateAll) {
554
560
  force_invalidation = true;
@@ -696,17 +702,18 @@ async function initialize(result, target, hydrate) {
696
702
 
697
703
  update(/** @type {import('@sveltejs/kit').Page} */ (result.props.page));
698
704
 
705
+ // TODO: use mount()
699
706
  root = new app.root({
700
707
  target,
701
- props: { ...result.props, stores, components },
708
+ props: { ...result.props, components },
702
709
  hydrate,
703
- // @ts-ignore Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync
710
+ // Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync
704
711
  sync: false,
705
- // @ts-ignore Svelte 5 specific: transformError allows to transform errors before they are passed to boundaries
712
+ // Svelte 5 specific: transformError allows to transform errors before they are passed to boundaries
706
713
  transformError: __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__
707
714
  ? /** @param {unknown} e */ async (e) => {
708
715
  const error = await handle_error(e, current.nav);
709
- rendering_error = { error, status: get_status(e) };
716
+ rendering_error = { error, status: error.status };
710
717
  page.error = error;
711
718
  page.status = rendering_error.status;
712
719
  return error;
@@ -718,8 +725,6 @@ async function initialize(result, target, hydrate) {
718
725
  // which causes component script blocks to run asynchronously
719
726
  void (await Promise.resolve());
720
727
 
721
- restore_snapshot(current_navigation_index);
722
-
723
728
  if (hydrate) {
724
729
  /** @type {import('@sveltejs/kit').AfterNavigate} */
725
730
  const navigation = {
@@ -736,6 +741,8 @@ async function initialize(result, target, hydrate) {
736
741
  after_navigate_callbacks.forEach((fn) => fn(navigation));
737
742
  }
738
743
 
744
+ restore_snapshot(current_navigation_index);
745
+
739
746
  started = true;
740
747
  }
741
748
 
@@ -746,7 +753,7 @@ async function initialize(result, target, hydrate) {
746
753
  * params: Record<string, string>;
747
754
  * branch: Array<import('./types.js').BranchNode | undefined>;
748
755
  * errors?: Array<import('types').CSRPageNodeLoader | undefined>;
749
- * status: number;
756
+ * status?: number;
750
757
  * error: App.Error | null;
751
758
  * route: import('types').CSRRoute | null;
752
759
  * form?: Record<string, any> | null;
@@ -790,9 +797,8 @@ async function get_navigation_result_from_branch({
790
797
  route
791
798
  },
792
799
  props: {
793
- // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
794
800
  constructors: compact(branch).map((branch_node) => branch_node.node.component),
795
- page: clone_page(page)
801
+ page
796
802
  }
797
803
  };
798
804
 
@@ -863,7 +869,7 @@ async function get_navigation_result_from_branch({
863
869
  id: route?.id ?? null
864
870
  },
865
871
  state: {},
866
- status,
872
+ status: status ?? error?.status ?? 200,
867
873
  url: new URL(url),
868
874
  form: form ?? null,
869
875
  // The whole page store is updated, but this way the object reference stays the same
@@ -1160,16 +1166,31 @@ function preload_error({ error, url, route, params }) {
1160
1166
  branch: []
1161
1167
  },
1162
1168
  props: {
1163
- page: clone_page(page),
1169
+ page: {
1170
+ // we skipped loading the error page, so we have to use the current page
1171
+ // store, but update the status received while preloading
1172
+ ...page,
1173
+ status: error.status
1174
+ },
1164
1175
  constructors: []
1165
1176
  }
1166
1177
  };
1167
1178
  }
1168
1179
 
1169
1180
  /**
1170
- * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1181
+ * @overload
1182
+ * @param {import('./types.js').NavigationIntent} intent
1183
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1184
+ */
1185
+ /**
1186
+ * @overload
1187
+ * @param {import('./types.js').NavigationIntent & { preload: {} }} intent
1171
1188
  * @returns {Promise<import('./types.js').NavigationResult>}
1172
1189
  */
1190
+ /**
1191
+ * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1192
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1193
+ */
1173
1194
  async function load_route({ id, invalidating, url, params, route, preload }) {
1174
1195
  if (load_cache?.id === id) {
1175
1196
  // the preload becomes the real navigation
@@ -1225,12 +1246,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1225
1246
  } catch (error) {
1226
1247
  const handled_error = await handle_error(error, { url, params, route: { id } });
1227
1248
 
1228
- if (preload_tokens.has(preload)) {
1249
+ if (preload && preload_tokens.has(preload)) {
1229
1250
  return preload_error({ error: handled_error, url, params, route });
1230
1251
  }
1231
1252
 
1232
1253
  return load_root_error_page({
1233
- status: get_status(error),
1234
1254
  error: handled_error,
1235
1255
  url,
1236
1256
  route
@@ -1273,7 +1293,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1273
1293
 
1274
1294
  if (server_data_node?.type === 'error') {
1275
1295
  // rethrow and catch below
1276
- throw server_data_node;
1296
+ throw new HttpError(server_data_node.error.status, server_data_node.error);
1277
1297
  }
1278
1298
 
1279
1299
  return load_node({
@@ -1311,34 +1331,33 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1311
1331
  if (err instanceof Redirect) {
1312
1332
  return {
1313
1333
  type: 'redirect',
1334
+ status: err.status,
1314
1335
  location: err.location
1315
1336
  };
1316
1337
  }
1317
1338
 
1318
- if (preload_tokens.has(preload)) {
1339
+ if (preload && preload_tokens.has(preload)) {
1340
+ const error = await handle_error(err, { params, url, route: { id: route.id } });
1319
1341
  return preload_error({
1320
- error: await handle_error(err, { params, url, route: { id: route.id } }),
1342
+ error,
1321
1343
  url,
1322
1344
  params,
1323
1345
  route
1324
1346
  });
1325
1347
  }
1326
1348
 
1327
- let status = get_status(err);
1328
1349
  /** @type {App.Error} */
1329
1350
  let error;
1330
1351
 
1331
1352
  if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
1332
1353
  // this is the server error rethrown above, reconstruct but don't invoke
1333
1354
  // the client error handler; it should've already been handled on the server
1334
- status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
1335
1355
  error = /** @type {import('types').ServerErrorNode} */ (err).error;
1336
1356
  } else if (err instanceof HttpError) {
1337
1357
  error = err.body;
1338
1358
  } else {
1339
1359
  // Referenced node could have been removed due to redeploy, check
1340
- const updated = await stores.updated.check();
1341
- if (updated) {
1360
+ if (await updated.check()) {
1342
1361
  // Before reloading, try to update the service worker if it exists
1343
1362
  await update_service_worker();
1344
1363
  return await native_navigation(url);
@@ -1354,12 +1373,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1354
1373
  params,
1355
1374
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
1356
1375
  errors,
1357
- status,
1358
1376
  error,
1359
1377
  route
1360
1378
  });
1361
1379
  } else {
1362
- return await server_fallback(url, { id: route.id }, error, status);
1380
+ return await server_fallback(url, { id: route.id }, error);
1363
1381
  }
1364
1382
  }
1365
1383
  } else {
@@ -1374,7 +1392,6 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1374
1392
  params,
1375
1393
  branch,
1376
1394
  errors,
1377
- status: 200,
1378
1395
  error: null,
1379
1396
  route,
1380
1397
  // Reset `form` on navigation, but not invalidation
@@ -1413,14 +1430,13 @@ async function load_nearest_error_page(i, branch, errors) {
1413
1430
 
1414
1431
  /**
1415
1432
  * @param {{
1416
- * status: number;
1417
1433
  * error: App.Error;
1418
1434
  * url: URL;
1419
1435
  * route: { id: string | null }
1420
1436
  * }} opts
1421
- * @returns {Promise<import('./types.js').NavigationFinished>}
1437
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>} returns `undefined` in case of a redirect
1422
1438
  */
1423
- async function load_root_error_page({ status, error, url, route }) {
1439
+ async function load_root_error_page({ error, url, route }) {
1424
1440
  /** @type {Record<string, string>} */
1425
1441
  const params = {}; // error page does not have params
1426
1442
 
@@ -1444,11 +1460,15 @@ async function load_root_error_page({ status, error, url, route }) {
1444
1460
  }
1445
1461
 
1446
1462
  server_data_node = server_data.nodes[0] ?? null;
1447
- } catch {
1463
+ } catch (e) {
1448
1464
  // at this point we have no choice but to fall back to the server, if it wouldn't
1449
- // bring us right back here, turning this into an endless loop
1450
- if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
1451
- await native_navigation(url);
1465
+ // bring us right back here, turning this into an endless loop.
1466
+ // if __data.json returned 404, the route doesn't exist don't reload or we loop
1467
+ if (
1468
+ !(e instanceof HttpError && e.status === 404) &&
1469
+ (url.origin !== origin || url.pathname !== location.pathname || hydrated)
1470
+ ) {
1471
+ return await native_navigation(url);
1452
1472
  }
1453
1473
  }
1454
1474
  }
@@ -1477,18 +1497,30 @@ async function load_root_error_page({ status, error, url, route }) {
1477
1497
  url,
1478
1498
  params,
1479
1499
  branch: [root_layout, root_error],
1480
- status,
1481
1500
  error,
1482
1501
  errors: [],
1483
1502
  route: null
1484
1503
  });
1485
1504
  } catch (error) {
1505
+ // client-side navigation if the root layout loader throws a redirect while
1506
+ // rendering the default error page
1486
1507
  if (error instanceof Redirect) {
1487
- // @ts-expect-error TODO investigate this
1488
- return _goto(new URL(error.location, location.href), {}, 0);
1508
+ await _goto(new URL(error.location, location.href), {}, 0);
1509
+ return;
1489
1510
  }
1490
1511
 
1491
- // TODO: this falls back to the server when a server exists, but what about SPA mode?
1512
+ // otherwise, render the static error page
1513
+ const error_template = await app.get_error_template();
1514
+ const handled = await handle_error(error, { url, params, route });
1515
+ const message = String(handled?.message ?? '')
1516
+ .replace(/&/g, '&amp;')
1517
+ .replace(/</g, '&lt;')
1518
+ .replace(/>/g, '&gt;');
1519
+ const html = error_template({ status: handled.status, message });
1520
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
1521
+ document.documentElement.replaceChild(document.adoptNode(parsed.head), document.head);
1522
+ document.documentElement.replaceChild(document.adoptNode(parsed.body), document.body);
1523
+
1492
1524
  throw error;
1493
1525
  }
1494
1526
  }
@@ -1578,7 +1610,7 @@ export async function get_navigation_intent(url, invalidating) {
1578
1610
  id: get_page_key(url),
1579
1611
  invalidating,
1580
1612
  route,
1581
- params: decode_params(params),
1613
+ params,
1582
1614
  url
1583
1615
  };
1584
1616
  }
@@ -1673,8 +1705,10 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1673
1705
  * nav_token?: {};
1674
1706
  * accept?: () => void;
1675
1707
  * block?: () => void;
1676
- * event?: Event
1708
+ * event?: Event;
1709
+ * intent?: NavigationIntent | undefined
1677
1710
  * }} opts
1711
+ * @returns {Promise<void>}
1678
1712
  */
1679
1713
  async function navigate({
1680
1714
  type,
@@ -1688,12 +1722,14 @@ async function navigate({
1688
1722
  nav_token = {},
1689
1723
  accept = noop,
1690
1724
  block = noop,
1691
- event
1725
+ event,
1726
+ intent
1692
1727
  }) {
1693
- const prev_token = token;
1694
- token = nav_token;
1728
+ const prev_token = navigation_token;
1729
+ const prev_invalidation_token = invalidation_token;
1730
+ navigation_token = invalidation_token = nav_token;
1695
1731
 
1696
- const intent = await get_navigation_intent(url, false);
1732
+ intent ??= await get_navigation_intent(url, false);
1697
1733
  const nav =
1698
1734
  type === 'enter'
1699
1735
  ? create_navigation(current, intent, url, type)
@@ -1709,7 +1745,8 @@ async function navigate({
1709
1745
 
1710
1746
  if (!nav) {
1711
1747
  block();
1712
- if (token === nav_token) token = prev_token;
1748
+ if (navigation_token === nav_token) navigation_token = prev_token;
1749
+ if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
1713
1750
  return;
1714
1751
  }
1715
1752
 
@@ -1722,7 +1759,7 @@ async function navigate({
1722
1759
  is_navigating = true;
1723
1760
 
1724
1761
  if (started && nav.navigation.type !== 'enter') {
1725
- stores.navigating.set((navigating.current = nav.navigation));
1762
+ navigating.current = nav.navigation;
1726
1763
  }
1727
1764
 
1728
1765
  let navigation_result = intent && (await load_route(intent));
@@ -1747,7 +1784,6 @@ async function navigate({
1747
1784
  route: { id: null }
1748
1785
  }
1749
1786
  ),
1750
- 404,
1751
1787
  replace_state
1752
1788
  );
1753
1789
  } else {
@@ -1762,7 +1798,6 @@ async function navigate({
1762
1798
  params: {},
1763
1799
  route: { id: null }
1764
1800
  }),
1765
- 404,
1766
1801
  replace_state
1767
1802
  );
1768
1803
  }
@@ -1773,11 +1808,13 @@ async function navigate({
1773
1808
  url = intent?.url || url;
1774
1809
 
1775
1810
  // abort if user navigated during update
1776
- if (token !== nav_token) {
1811
+ if (navigation_token !== nav_token) {
1777
1812
  nav.reject(new Error('navigation aborted'));
1778
- return false;
1813
+ return;
1779
1814
  }
1780
1815
 
1816
+ if (!navigation_result) return;
1817
+
1781
1818
  if (navigation_result.type === 'redirect') {
1782
1819
  // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1783
1820
  if (redirect_count < 20) {
@@ -1798,7 +1835,6 @@ async function navigate({
1798
1835
  }
1799
1836
 
1800
1837
  navigation_result = await load_root_error_page({
1801
- status: 500,
1802
1838
  error: await handle_error(new Error('Redirect loop'), {
1803
1839
  url,
1804
1840
  params: {},
@@ -1807,12 +1843,13 @@ async function navigate({
1807
1843
  url,
1808
1844
  route: { id: null }
1809
1845
  });
1846
+
1847
+ if (!navigation_result) return;
1810
1848
  } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1811
- const updated = await stores.updated.check();
1812
- if (updated) {
1849
+ if (await updated.check()) {
1813
1850
  // Before reloading, try to update the service worker if it exists
1814
1851
  await update_service_worker();
1815
- await native_navigation(url, replace_state);
1852
+ return await native_navigation(url, replace_state);
1816
1853
  }
1817
1854
  }
1818
1855
 
@@ -1907,6 +1944,16 @@ async function navigate({
1907
1944
  navigation_result.props.page.url = url;
1908
1945
  }
1909
1946
 
1947
+ // Remove focus before updating the component tree, so that blur/focusout
1948
+ // handlers fire while the old component's data is still valid (#14575)
1949
+ if (
1950
+ !keepfocus &&
1951
+ document.activeElement instanceof HTMLElement &&
1952
+ document.activeElement !== document.body
1953
+ ) {
1954
+ document.activeElement.blur();
1955
+ }
1956
+
1910
1957
  const fork = load_cache_fork && (await load_cache_fork);
1911
1958
 
1912
1959
  if (fork) {
@@ -1930,18 +1977,12 @@ async function navigate({
1930
1977
 
1931
1978
  const { activeElement } = document;
1932
1979
 
1933
- await commit_promise;
1934
-
1935
- // TODO 3.0 remote — the double tick is probably necessary because
1936
- // of some store shenanigans. `settled()` and `f.commit()`
1937
- // should resolve after DOM updates in newer versions
1938
- await svelte.tick();
1939
- await svelte.tick();
1980
+ await (commit_promise ?? svelte.tick());
1940
1981
 
1941
- if (token !== nav_token) {
1982
+ if (navigation_token !== nav_token) {
1942
1983
  // a new navigation happened while we were waiting for the DOM to update, so abort
1943
1984
  nav.reject(new Error('navigation aborted'));
1944
- return false;
1985
+ return;
1945
1986
  }
1946
1987
 
1947
1988
  // Check for async rendering error
@@ -1985,10 +2026,6 @@ async function navigate({
1985
2026
 
1986
2027
  is_navigating = false;
1987
2028
 
1988
- if (type === 'popstate') {
1989
- restore_snapshot(current_navigation_index);
1990
- }
1991
-
1992
2029
  nav.fulfil(undefined);
1993
2030
 
1994
2031
  // Update to.scroll to the actual scroll position after navigation completed
@@ -2000,7 +2037,11 @@ async function navigate({
2000
2037
  fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
2001
2038
  );
2002
2039
 
2003
- stores.navigating.set((navigating.current = null));
2040
+ if (type === 'popstate') {
2041
+ restore_snapshot(current_navigation_index);
2042
+ }
2043
+
2044
+ navigating.current = null;
2004
2045
 
2005
2046
  updating = false;
2006
2047
  }
@@ -2010,23 +2051,21 @@ async function navigate({
2010
2051
  * @param {URL} url
2011
2052
  * @param {{ id: string | null }} route
2012
2053
  * @param {App.Error} error
2013
- * @param {number} status
2014
2054
  * @param {boolean} [replace_state]
2015
- * @returns {Promise<import('./types.js').NavigationFinished>}
2055
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>}
2016
2056
  */
2017
- async function server_fallback(url, route, error, status, replace_state) {
2057
+ async function server_fallback(url, route, error, replace_state) {
2018
2058
  if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
2019
2059
  // We would reload the same page we're currently on, which isn't hydrated,
2020
2060
  // which means no SSR, which means we would end up in an endless loop
2021
2061
  return await load_root_error_page({
2022
- status,
2023
2062
  error,
2024
2063
  url,
2025
2064
  route
2026
2065
  });
2027
2066
  }
2028
2067
 
2029
- if (DEV && status !== 404) {
2068
+ if (DEV && error.status !== 404) {
2030
2069
  console.error(
2031
2070
  'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
2032
2071
  );
@@ -2165,9 +2204,9 @@ function setup_preload() {
2165
2204
  /**
2166
2205
  * @param {unknown} error
2167
2206
  * @param {import('@sveltejs/kit').NavigationEvent} event
2168
- * @returns {import('types').MaybePromise<App.Error>}
2207
+ * @returns {Promise<App.Error>}
2169
2208
  */
2170
- function handle_error(error, event) {
2209
+ export async function handle_error(error, event) {
2171
2210
  if (error instanceof HttpError) {
2172
2211
  return error.body;
2173
2212
  }
@@ -2179,10 +2218,9 @@ function handle_error(error, event) {
2179
2218
 
2180
2219
  const status = get_status(error);
2181
2220
  const message = get_message(error);
2221
+ const app_error = (await app.hooks.handleError({ error, event, status, message })) ?? { message };
2182
2222
 
2183
- return (
2184
- app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
2185
- );
2223
+ return { ...app_error, status: get_status(app_error, error) };
2186
2224
  }
2187
2225
 
2188
2226
  /**
@@ -2191,7 +2229,7 @@ function handle_error(error, event) {
2191
2229
  * @param {T} callback
2192
2230
  */
2193
2231
  function add_navigation_callback(callbacks, callback) {
2194
- onMount(() => {
2232
+ svelte.onMount(() => {
2195
2233
  callbacks.add(callback);
2196
2234
 
2197
2235
  return () => {
@@ -2266,9 +2304,11 @@ export function disableScrollHandling() {
2266
2304
  * Allows you to navigate programmatically to a given route, with options such as keeping the current element focused.
2267
2305
  * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
2268
2306
  *
2269
- * For external URLs, use `window.location = url` instead of calling `goto(url)`.
2307
+ * `goto` is intended for navigations to routes that belong to the app.
2308
+ * If the URL does not resolve to a route within the app, the returned promise will reject.
2309
+ * For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
2270
2310
  *
2271
- * @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.
2311
+ * @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.
2272
2312
  * @param {Object} [opts] Options related to the navigation
2273
2313
  * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
2274
2314
  * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation
@@ -2278,7 +2318,7 @@ export function disableScrollHandling() {
2278
2318
  * @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
2279
2319
  * @returns {Promise<void>}
2280
2320
  */
2281
- export function goto(url, opts = {}) {
2321
+ export async function goto(url, opts = {}) {
2282
2322
  if (!BROWSER) {
2283
2323
  throw new Error('Cannot call goto(...) on the server');
2284
2324
  }
@@ -2286,16 +2326,24 @@ export function goto(url, opts = {}) {
2286
2326
  url = new URL(resolve_url(url));
2287
2327
 
2288
2328
  if (url.origin !== origin) {
2289
- return Promise.reject(
2290
- new Error(
2291
- DEV
2292
- ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2293
- : 'goto: invalid URL'
2294
- )
2329
+ throw new Error(
2330
+ DEV
2331
+ ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2332
+ : 'goto: invalid URL'
2295
2333
  );
2296
2334
  }
2297
2335
 
2298
- return _goto(url, opts, 0);
2336
+ const intent = await get_navigation_intent(url, false);
2337
+
2338
+ if (!intent) {
2339
+ throw new Error(
2340
+ DEV
2341
+ ? `Cannot use \`goto\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
2342
+ : 'goto: invalid URL'
2343
+ );
2344
+ }
2345
+
2346
+ return _goto(url, opts, 0, {}, intent);
2299
2347
  }
2300
2348
 
2301
2349
  /**
@@ -2376,7 +2424,7 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
2376
2424
  * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
2377
2425
  *
2378
2426
  * @param {string} href Page to preload
2379
- * @returns {Promise<{ type: 'loaded'; status: number; data: Record<string, any> } | { type: 'redirect'; location: string }>}
2427
+ * @returns {Promise<({ type: 'loaded'; data: Record<string, any> } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>}
2380
2428
  */
2381
2429
  export async function preloadData(href) {
2382
2430
  if (!BROWSER) {
@@ -2394,11 +2442,21 @@ export async function preloadData(href) {
2394
2442
  if (result.type === 'redirect') {
2395
2443
  return {
2396
2444
  type: result.type,
2445
+ status: result.status,
2397
2446
  location: result.location
2398
2447
  };
2399
2448
  }
2400
2449
 
2401
2450
  const { status, data } = result.props.page ?? page;
2451
+
2452
+ if (result.type === 'loaded' && result.state.error) {
2453
+ return {
2454
+ type: 'error',
2455
+ status,
2456
+ error: result.state.error
2457
+ };
2458
+ }
2459
+
2402
2460
  return { type: result.type, status, data };
2403
2461
  }
2404
2462
 
@@ -2419,7 +2477,9 @@ export async function preloadCode(pathname) {
2419
2477
  throw new Error('Cannot call preloadCode(...) on the server');
2420
2478
  }
2421
2479
 
2422
- const url = new URL(pathname, current.url);
2480
+ // `current.url` is null until the first navigation/hydration completes, so fall back
2481
+ // to `location` to support calling `preloadCode` during initial page load (#13297)
2482
+ const url = new URL(pathname, current.url ?? location.href);
2423
2483
 
2424
2484
  if (DEV) {
2425
2485
  if (!pathname.startsWith('/')) {
@@ -2485,8 +2545,7 @@ export function pushState(url, state) {
2485
2545
 
2486
2546
  page.state = state;
2487
2547
  root.$set({
2488
- // we need to assign a new page object so that subscribers are correctly notified
2489
- page: untrack(() => clone_page(page))
2548
+ page
2490
2549
  });
2491
2550
 
2492
2551
  clear_onward_history(current_history_index, current_navigation_index);
@@ -2529,7 +2588,7 @@ export function replaceState(url, state) {
2529
2588
 
2530
2589
  page.state = state;
2531
2590
  root.$set({
2532
- page: untrack(() => clone_page(page))
2591
+ page
2533
2592
  });
2534
2593
  }
2535
2594
 
@@ -2547,7 +2606,7 @@ export async function applyAction(result) {
2547
2606
  }
2548
2607
 
2549
2608
  if (result.type === 'error') {
2550
- await set_nearest_error_page(result.error, result.status);
2609
+ await set_nearest_error_page(result.error);
2551
2610
  } else if (result.type === 'redirect') {
2552
2611
  await _goto(result.location, { invalidateAll: true }, 0);
2553
2612
  } else {
@@ -2559,11 +2618,11 @@ export async function applyAction(result) {
2559
2618
  // this brings Svelte's view of the world in line with SvelteKit's
2560
2619
  // after use:enhance reset the form....
2561
2620
  form: null,
2562
- page: clone_page(page)
2621
+ page
2563
2622
  });
2564
2623
 
2565
2624
  // ...so that setting the `form` prop takes effect and isn't ignored
2566
- await tick();
2625
+ await svelte.tick();
2567
2626
  root.$set({ form: result.data });
2568
2627
 
2569
2628
  if (result.type === 'success') {
@@ -2574,9 +2633,8 @@ export async function applyAction(result) {
2574
2633
 
2575
2634
  /**
2576
2635
  * @param {App.Error} error
2577
- * @param {number} status
2578
2636
  */
2579
- export async function set_nearest_error_page(error, status = 500) {
2637
+ export async function set_nearest_error_page(error) {
2580
2638
  const url = new URL(location.href);
2581
2639
 
2582
2640
  const { branch, route } = current;
@@ -2588,7 +2646,6 @@ export async function set_nearest_error_page(error, status = 500) {
2588
2646
  url,
2589
2647
  params: current.params,
2590
2648
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
2591
- status,
2592
2649
  error,
2593
2650
  // do not set errors, we haven't changed the page so the previous ones are still current
2594
2651
  route
@@ -2599,7 +2656,7 @@ export async function set_nearest_error_page(error, status = 500) {
2599
2656
  root.$set(navigation_result.props);
2600
2657
  update(navigation_result.props.page);
2601
2658
 
2602
- void tick().then(() => reset_focus(current.url));
2659
+ void svelte.tick().then(() => reset_focus(current.url));
2603
2660
  }
2604
2661
  }
2605
2662
 
@@ -2823,7 +2880,7 @@ function _start_router() {
2823
2880
 
2824
2881
  if (event.state?.[HISTORY_INDEX]) {
2825
2882
  const history_index = event.state[HISTORY_INDEX];
2826
- token = {};
2883
+ navigation_token = invalidation_token = {};
2827
2884
 
2828
2885
  // if a popstate-driven navigation is cancelled, we need to counteract it
2829
2886
  // with history.go, which means we end up back here, hence this check
@@ -2872,7 +2929,7 @@ function _start_router() {
2872
2929
  block: () => {
2873
2930
  history.go(-delta);
2874
2931
  },
2875
- nav_token: token,
2932
+ nav_token: navigation_token,
2876
2933
  event
2877
2934
  });
2878
2935
  } else {
@@ -2924,7 +2981,7 @@ function _start_router() {
2924
2981
  // the navigation away from it was successful.
2925
2982
  // Info about bfcache here: https://web.dev/bfcache
2926
2983
  if (event.persisted) {
2927
- stores.navigating.set((navigating.current = null));
2984
+ navigating.current = null;
2928
2985
  }
2929
2986
  });
2930
2987
 
@@ -2933,18 +2990,17 @@ function _start_router() {
2933
2990
  */
2934
2991
  function update_url(url) {
2935
2992
  current.url = page.url = url;
2936
- stores.page.set(clone_page(page));
2937
- stores.page.notify();
2938
2993
  }
2939
2994
  }
2940
2995
 
2941
2996
  /**
2942
2997
  * @param {HTMLElement} target
2943
2998
  * @param {import('./types.js').HydrateOptions} opts
2999
+ * @returns {Promise<void>}
2944
3000
  */
2945
3001
  async function _hydrate(
2946
3002
  target,
2947
- { status = 200, error, node_ids, params, route, server_route, data: server_data_nodes, form }
3003
+ { status, error, node_ids, params, route, server_route, data: server_data_nodes, form }
2948
3004
  ) {
2949
3005
  hydrated = true;
2950
3006
 
@@ -3027,13 +3083,13 @@ async function _hydrate(
3027
3083
  if (error instanceof Redirect) {
3028
3084
  // this is a real edge case — `load` would need to return
3029
3085
  // a redirect but only in the browser
3030
- await native_navigation(new URL(error.location, location.href));
3031
- return;
3086
+ return await native_navigation(new URL(error.location, location.href));
3032
3087
  }
3033
3088
 
3089
+ const handled_error = await handle_error(error, { url, params, route });
3090
+
3034
3091
  result = await load_root_error_page({
3035
- status: get_status(error),
3036
- error: await handle_error(error, { url, params, route }),
3092
+ error: handled_error,
3037
3093
  url,
3038
3094
  route
3039
3095
  });
@@ -3042,6 +3098,10 @@ async function _hydrate(
3042
3098
  hydrate = false;
3043
3099
  }
3044
3100
 
3101
+ // Exit early when we encounter a redirect while loading the root error page.
3102
+ // In this case, `initialize` will be called later on
3103
+ if (!result) return;
3104
+
3045
3105
  if (result.props.page) {
3046
3106
  result.props.page.state = {};
3047
3107
  }
@@ -3323,28 +3383,6 @@ function create_navigation(current, intent, url, type, target_scroll = null) {
3323
3383
  };
3324
3384
  }
3325
3385
 
3326
- /**
3327
- * TODO: remove this in 3.0 when the page store is also removed
3328
- *
3329
- * We need to assign a new page object so that subscribers are correctly notified.
3330
- * However, spreading `{ ...page }` returns an empty object so we manually
3331
- * assign to each property instead.
3332
- *
3333
- * @param {import('@sveltejs/kit').Page} page
3334
- */
3335
- function clone_page(page) {
3336
- return {
3337
- data: page.data,
3338
- error: page.error,
3339
- form: page.form,
3340
- params: page.params,
3341
- route: page.route,
3342
- state: page.state,
3343
- status: page.status,
3344
- url: page.url
3345
- };
3346
- }
3347
-
3348
3386
  /**
3349
3387
  * @param {URL} url
3350
3388
  * @returns {URL}