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

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 (68) hide show
  1. package/package.json +17 -17
  2. package/src/core/adapt/builder.js +18 -4
  3. package/src/core/adapt/index.js +1 -4
  4. package/src/core/config/index.js +64 -5
  5. package/src/core/config/options.js +73 -21
  6. package/src/core/env.js +35 -4
  7. package/src/core/postbuild/analyse.js +8 -12
  8. package/src/core/postbuild/crawl.js +22 -6
  9. package/src/core/postbuild/prerender.js +40 -23
  10. package/src/core/sync/create_manifest_data/index.js +23 -5
  11. package/src/core/sync/write_client_manifest.js +7 -0
  12. package/src/core/sync/write_server.js +13 -10
  13. package/src/core/sync/write_tsconfig.js +2 -4
  14. package/src/exports/index.js +32 -10
  15. package/src/exports/internal/env.js +1 -1
  16. package/src/exports/internal/index.js +6 -5
  17. package/src/exports/node/index.js +57 -6
  18. package/src/exports/public.d.ts +218 -114
  19. package/src/exports/vite/build/build_server.js +6 -1
  20. package/src/exports/vite/dev/index.js +10 -20
  21. package/src/exports/vite/index.js +306 -222
  22. package/src/exports/vite/preview/index.js +15 -7
  23. package/src/exports/vite/utils.js +8 -10
  24. package/src/runtime/app/env/types.d.ts +1 -1
  25. package/src/runtime/app/forms.js +22 -5
  26. package/src/runtime/app/paths/client.js +1 -3
  27. package/src/runtime/app/paths/public.d.ts +0 -28
  28. package/src/runtime/app/paths/server.js +7 -3
  29. package/src/runtime/app/server/remote/form.js +10 -3
  30. package/src/runtime/app/server/remote/query.js +3 -6
  31. package/src/runtime/app/server/remote/requested.js +8 -4
  32. package/src/runtime/app/server/remote/shared.js +5 -7
  33. package/src/runtime/client/client.js +184 -93
  34. package/src/runtime/client/fetcher.js +3 -2
  35. package/src/runtime/client/remote-functions/form.svelte.js +134 -24
  36. package/src/runtime/client/remote-functions/prerender.svelte.js +8 -2
  37. package/src/runtime/client/remote-functions/query/index.js +3 -2
  38. package/src/runtime/client/remote-functions/query/instance.svelte.js +9 -2
  39. package/src/runtime/client/remote-functions/query-batch.svelte.js +4 -3
  40. package/src/runtime/client/remote-functions/query-live/instance.svelte.js +26 -9
  41. package/src/runtime/client/remote-functions/shared.svelte.js +17 -7
  42. package/src/runtime/client/types.d.ts +9 -1
  43. package/src/runtime/client/utils.js +1 -1
  44. package/src/runtime/form-utils.js +84 -16
  45. package/src/runtime/server/cookie.js +22 -33
  46. package/src/runtime/server/csrf.js +65 -0
  47. package/src/runtime/server/data/index.js +7 -7
  48. package/src/runtime/server/env_module.js +0 -5
  49. package/src/runtime/server/index.js +1 -1
  50. package/src/runtime/server/page/actions.js +41 -26
  51. package/src/runtime/server/page/index.js +2 -1
  52. package/src/runtime/server/page/render.js +21 -23
  53. package/src/runtime/server/page/respond_with_error.js +7 -8
  54. package/src/runtime/server/remote.js +64 -27
  55. package/src/runtime/server/respond.js +81 -50
  56. package/src/runtime/server/utils.js +7 -7
  57. package/src/runtime/telemetry/otel.js +1 -1
  58. package/src/types/ambient.d.ts +5 -4
  59. package/src/types/global-private.d.ts +4 -4
  60. package/src/types/internal.d.ts +8 -10
  61. package/src/types/private.d.ts +33 -1
  62. package/src/types/synthetic/$lib.md +1 -1
  63. package/src/utils/error.js +11 -3
  64. package/src/utils/shared-iterator.js +5 -0
  65. package/src/utils/url.js +99 -2
  66. package/src/version.js +1 -1
  67. package/types/index.d.ts +340 -186
  68. package/types/index.d.ts.map +10 -9
@@ -1,4 +1,5 @@
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' */
@@ -30,7 +31,7 @@ import {
30
31
  create_updated_store,
31
32
  load_css
32
33
  } from './utils.js';
33
- import { base } from '$app/paths';
34
+ import { base } from '$app/paths/internal/client';
34
35
  import * as devalue from 'devalue';
35
36
  import {
36
37
  HISTORY_INDEX,
@@ -90,8 +91,6 @@ const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
90
91
  if (DEV && BROWSER) {
91
92
  let warned = false;
92
93
 
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
94
  const warn = () => {
96
95
  if (warned) return;
97
96
 
@@ -100,9 +99,13 @@ if (DEV && BROWSER) {
100
99
  let stack = new Error().stack?.split('\n');
101
100
  if (!stack) return;
102
101
  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;
102
+
103
+ // skip over `warn` and the place where `warn` was called
104
+ const frame = stack[2];
105
+
106
+ // ignore calls that happen inside dependencies, including SvelteKit.
107
+ // `frame` can be falsy if we came from an anonymous function
108
+ if (frame?.includes('node_modules')) return;
106
109
 
107
110
  warned = true;
108
111
 
@@ -163,7 +166,8 @@ function clear_onward_history(current_history_index, current_navigation_index) {
163
166
  * Returns a `Promise` that never resolves (to prevent any
164
167
  * subsequent work, e.g. history manipulation, from happening)
165
168
  * @param {URL} url
166
- * @param {boolean} [replace] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
169
+ * @param {boolean} [replace] if `true`, will replace the current `history` entry rather than creating a new one with `pushState`
170
+ * @returns {Promise<any>} a promise that never resolves
167
171
  */
168
172
  function native_navigation(url, replace = false) {
169
173
  if (replace) {
@@ -187,7 +191,7 @@ async function update_service_worker() {
187
191
  }
188
192
  }
189
193
 
190
- /** @type {import('types').CSRRoute[]} All routes of the app. Only available when kit.router.resolution=client */
194
+ /** @type {import('types').CSRRoute[]} All routes of the app. Only available when router.resolution=client */
191
195
  let routes;
192
196
  /** @type {import('types').CSRPageNodeLoader} */
193
197
  let default_layout_loader;
@@ -292,8 +296,17 @@ let current_history_index;
292
296
  /** @type {number} */
293
297
  let current_navigation_index;
294
298
 
295
- /** @type {{}} */
296
- let token;
299
+ /** @type {{}} Token for the latest navigation. Updated on new navigations */
300
+ let navigation_token;
301
+
302
+ /**
303
+ * @type {{}}
304
+ * The latest invalidate(All) token. Superseeded by both later invalidate(All)s and navigations.
305
+ * This is separate to navigation_token because an invalidate(All) might be triggered while a navigation
306
+ * is in progress, and we want to be able to finish this navigation (unless the invalidation finishes before
307
+ * it and redirects, in which case we will do the redirect triggered by the invalidation).
308
+ */
309
+ let invalidation_token;
297
310
 
298
311
  /**
299
312
  * A set of tokens which are associated to current preloads.
@@ -301,6 +314,7 @@ let token;
301
314
  * If a preload token is in the set and the preload errors, the error
302
315
  * handling logic (for example reloading) is skipped.
303
316
  */
317
+ /** @type {Set<{}>} */
304
318
  const preload_tokens = new Set();
305
319
 
306
320
  /** @type {Promise<void> | null} */
@@ -418,7 +432,9 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
418
432
  if (!pending_invalidate) return;
419
433
  pending_invalidate = null;
420
434
 
421
- const nav_token = (token = {});
435
+ const token = (invalidation_token = {});
436
+ const nav_token = navigation_token;
437
+ const navigating = is_navigating;
422
438
  const intent = await get_navigation_intent(current.url, true);
423
439
 
424
440
  // Clear preload, it might be affected by the invalidation.
@@ -450,17 +466,25 @@ async function _invalidate(include_load_functions = true, reset_page_state = tru
450
466
  if (include_load_functions) {
451
467
  const prev_state = page.state;
452
468
  const navigation_result = intent && (await load_route(intent));
453
- if (!navigation_result || nav_token !== token) return;
469
+ if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
470
+ return;
471
+ }
454
472
 
455
473
  if (navigation_result.type === 'redirect') {
456
474
  return _goto(
457
475
  new URL(navigation_result.location, current.url).href,
458
476
  { replaceState: true },
459
477
  1,
460
- nav_token
478
+ token
461
479
  );
462
480
  }
463
481
 
482
+ // A navigation started before the invalidation and ended before it finished. The invalidation did not redirect,
483
+ // hence it likely contains outdated data now, so we ignore it.
484
+ if (navigating && !is_navigating) {
485
+ return;
486
+ }
487
+
464
488
  // This is a bit hacky but allows us not having to pass that boolean around, making things harder to reason about
465
489
  if (!reset_page_state) {
466
490
  navigation_result.props.page.state = prev_state;
@@ -527,8 +551,10 @@ function persist_state() {
527
551
  * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} options
528
552
  * @param {number} redirect_count
529
553
  * @param {{}} [nav_token]
554
+ * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
555
+ * @returns {Promise<void>}
530
556
  */
531
- export async function _goto(url, options, redirect_count, nav_token) {
557
+ export async function _goto(url, options, redirect_count, nav_token, intent) {
532
558
  /** @type {Set<string>} */
533
559
  let query_keys;
534
560
  /** @type {Set<string>} */
@@ -549,6 +575,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
549
575
  state: options.state,
550
576
  redirect_count,
551
577
  nav_token,
578
+ intent,
552
579
  accept: () => {
553
580
  if (options.invalidateAll) {
554
581
  force_invalidation = true;
@@ -706,7 +733,7 @@ async function initialize(result, target, hydrate) {
706
733
  transformError: __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__
707
734
  ? /** @param {unknown} e */ async (e) => {
708
735
  const error = await handle_error(e, current.nav);
709
- rendering_error = { error, status: get_status(e) };
736
+ rendering_error = { error, status: error.status };
710
737
  page.error = error;
711
738
  page.status = rendering_error.status;
712
739
  return error;
@@ -718,8 +745,6 @@ async function initialize(result, target, hydrate) {
718
745
  // which causes component script blocks to run asynchronously
719
746
  void (await Promise.resolve());
720
747
 
721
- restore_snapshot(current_navigation_index);
722
-
723
748
  if (hydrate) {
724
749
  /** @type {import('@sveltejs/kit').AfterNavigate} */
725
750
  const navigation = {
@@ -736,6 +761,8 @@ async function initialize(result, target, hydrate) {
736
761
  after_navigate_callbacks.forEach((fn) => fn(navigation));
737
762
  }
738
763
 
764
+ restore_snapshot(current_navigation_index);
765
+
739
766
  started = true;
740
767
  }
741
768
 
@@ -746,7 +773,7 @@ async function initialize(result, target, hydrate) {
746
773
  * params: Record<string, string>;
747
774
  * branch: Array<import('./types.js').BranchNode | undefined>;
748
775
  * errors?: Array<import('types').CSRPageNodeLoader | undefined>;
749
- * status: number;
776
+ * status?: number;
750
777
  * error: App.Error | null;
751
778
  * route: import('types').CSRRoute | null;
752
779
  * form?: Record<string, any> | null;
@@ -863,7 +890,7 @@ async function get_navigation_result_from_branch({
863
890
  id: route?.id ?? null
864
891
  },
865
892
  state: {},
866
- status,
893
+ status: status ?? error?.status ?? 200,
867
894
  url: new URL(url),
868
895
  form: form ?? null,
869
896
  // The whole page store is updated, but this way the object reference stays the same
@@ -1150,6 +1177,10 @@ function diff_search_params(old_url, new_url) {
1150
1177
  * @returns {import('./types.js').NavigationFinished}
1151
1178
  */
1152
1179
  function preload_error({ error, url, route, params }) {
1180
+ // we skipped loading the error page, so we need to use the current page
1181
+ // store, but we still pass the updated status to the preloadData function
1182
+ const new_page = clone_page(page);
1183
+ new_page.status = error.status;
1153
1184
  return {
1154
1185
  type: 'loaded',
1155
1186
  state: {
@@ -1160,16 +1191,26 @@ function preload_error({ error, url, route, params }) {
1160
1191
  branch: []
1161
1192
  },
1162
1193
  props: {
1163
- page: clone_page(page),
1194
+ page: new_page,
1164
1195
  constructors: []
1165
1196
  }
1166
1197
  };
1167
1198
  }
1168
1199
 
1169
1200
  /**
1170
- * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1201
+ * @overload
1202
+ * @param {import('./types.js').NavigationIntent} intent
1203
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1204
+ */
1205
+ /**
1206
+ * @overload
1207
+ * @param {import('./types.js').NavigationIntent & { preload: {} }} intent
1171
1208
  * @returns {Promise<import('./types.js').NavigationResult>}
1172
1209
  */
1210
+ /**
1211
+ * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1212
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1213
+ */
1173
1214
  async function load_route({ id, invalidating, url, params, route, preload }) {
1174
1215
  if (load_cache?.id === id) {
1175
1216
  // the preload becomes the real navigation
@@ -1225,12 +1266,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1225
1266
  } catch (error) {
1226
1267
  const handled_error = await handle_error(error, { url, params, route: { id } });
1227
1268
 
1228
- if (preload_tokens.has(preload)) {
1269
+ if (preload && preload_tokens.has(preload)) {
1229
1270
  return preload_error({ error: handled_error, url, params, route });
1230
1271
  }
1231
1272
 
1232
1273
  return load_root_error_page({
1233
- status: get_status(error),
1234
1274
  error: handled_error,
1235
1275
  url,
1236
1276
  route
@@ -1273,7 +1313,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1273
1313
 
1274
1314
  if (server_data_node?.type === 'error') {
1275
1315
  // rethrow and catch below
1276
- throw server_data_node;
1316
+ throw new HttpError(server_data_node.error.status, server_data_node.error);
1277
1317
  }
1278
1318
 
1279
1319
  return load_node({
@@ -1311,27 +1351,27 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1311
1351
  if (err instanceof Redirect) {
1312
1352
  return {
1313
1353
  type: 'redirect',
1354
+ status: err.status,
1314
1355
  location: err.location
1315
1356
  };
1316
1357
  }
1317
1358
 
1318
- if (preload_tokens.has(preload)) {
1359
+ if (preload && preload_tokens.has(preload)) {
1360
+ const error = await handle_error(err, { params, url, route: { id: route.id } });
1319
1361
  return preload_error({
1320
- error: await handle_error(err, { params, url, route: { id: route.id } }),
1362
+ error,
1321
1363
  url,
1322
1364
  params,
1323
1365
  route
1324
1366
  });
1325
1367
  }
1326
1368
 
1327
- let status = get_status(err);
1328
1369
  /** @type {App.Error} */
1329
1370
  let error;
1330
1371
 
1331
1372
  if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
1332
1373
  // this is the server error rethrown above, reconstruct but don't invoke
1333
1374
  // the client error handler; it should've already been handled on the server
1334
- status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
1335
1375
  error = /** @type {import('types').ServerErrorNode} */ (err).error;
1336
1376
  } else if (err instanceof HttpError) {
1337
1377
  error = err.body;
@@ -1354,12 +1394,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1354
1394
  params,
1355
1395
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
1356
1396
  errors,
1357
- status,
1358
1397
  error,
1359
1398
  route
1360
1399
  });
1361
1400
  } else {
1362
- return await server_fallback(url, { id: route.id }, error, status);
1401
+ return await server_fallback(url, { id: route.id }, error);
1363
1402
  }
1364
1403
  }
1365
1404
  } else {
@@ -1374,7 +1413,6 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1374
1413
  params,
1375
1414
  branch,
1376
1415
  errors,
1377
- status: 200,
1378
1416
  error: null,
1379
1417
  route,
1380
1418
  // Reset `form` on navigation, but not invalidation
@@ -1413,14 +1451,13 @@ async function load_nearest_error_page(i, branch, errors) {
1413
1451
 
1414
1452
  /**
1415
1453
  * @param {{
1416
- * status: number;
1417
1454
  * error: App.Error;
1418
1455
  * url: URL;
1419
1456
  * route: { id: string | null }
1420
1457
  * }} opts
1421
- * @returns {Promise<import('./types.js').NavigationFinished>}
1458
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>} returns `undefined` in case of a redirect
1422
1459
  */
1423
- async function load_root_error_page({ status, error, url, route }) {
1460
+ async function load_root_error_page({ error, url, route }) {
1424
1461
  /** @type {Record<string, string>} */
1425
1462
  const params = {}; // error page does not have params
1426
1463
 
@@ -1444,11 +1481,15 @@ async function load_root_error_page({ status, error, url, route }) {
1444
1481
  }
1445
1482
 
1446
1483
  server_data_node = server_data.nodes[0] ?? null;
1447
- } catch {
1484
+ } catch (e) {
1448
1485
  // 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);
1486
+ // bring us right back here, turning this into an endless loop.
1487
+ // if __data.json returned 404, the route doesn't exist don't reload or we loop
1488
+ if (
1489
+ !(e instanceof HttpError && e.status === 404) &&
1490
+ (url.origin !== origin || url.pathname !== location.pathname || hydrated)
1491
+ ) {
1492
+ return await native_navigation(url);
1452
1493
  }
1453
1494
  }
1454
1495
  }
@@ -1477,18 +1518,30 @@ async function load_root_error_page({ status, error, url, route }) {
1477
1518
  url,
1478
1519
  params,
1479
1520
  branch: [root_layout, root_error],
1480
- status,
1481
1521
  error,
1482
1522
  errors: [],
1483
1523
  route: null
1484
1524
  });
1485
1525
  } catch (error) {
1526
+ // client-side navigation if the root layout loader throws a redirect while
1527
+ // rendering the default error page
1486
1528
  if (error instanceof Redirect) {
1487
- // @ts-expect-error TODO investigate this
1488
- return _goto(new URL(error.location, location.href), {}, 0);
1529
+ await _goto(new URL(error.location, location.href), {}, 0);
1530
+ return;
1489
1531
  }
1490
1532
 
1491
- // TODO: this falls back to the server when a server exists, but what about SPA mode?
1533
+ // otherwise, render the static error page
1534
+ const error_template = await app.get_error_template();
1535
+ const handled = await handle_error(error, { url, params, route });
1536
+ const message = String(handled?.message ?? '')
1537
+ .replace(/&/g, '&amp;')
1538
+ .replace(/</g, '&lt;')
1539
+ .replace(/>/g, '&gt;');
1540
+ const html = error_template({ status: handled.status, message });
1541
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
1542
+ document.documentElement.replaceChild(document.adoptNode(parsed.head), document.head);
1543
+ document.documentElement.replaceChild(document.adoptNode(parsed.body), document.body);
1544
+
1492
1545
  throw error;
1493
1546
  }
1494
1547
  }
@@ -1673,8 +1726,10 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1673
1726
  * nav_token?: {};
1674
1727
  * accept?: () => void;
1675
1728
  * block?: () => void;
1676
- * event?: Event
1729
+ * event?: Event;
1730
+ * intent?: NavigationIntent | undefined
1677
1731
  * }} opts
1732
+ * @returns {Promise<void>}
1678
1733
  */
1679
1734
  async function navigate({
1680
1735
  type,
@@ -1688,12 +1743,14 @@ async function navigate({
1688
1743
  nav_token = {},
1689
1744
  accept = noop,
1690
1745
  block = noop,
1691
- event
1746
+ event,
1747
+ intent
1692
1748
  }) {
1693
- const prev_token = token;
1694
- token = nav_token;
1749
+ const prev_token = navigation_token;
1750
+ const prev_invalidation_token = invalidation_token;
1751
+ navigation_token = invalidation_token = nav_token;
1695
1752
 
1696
- const intent = await get_navigation_intent(url, false);
1753
+ intent ??= await get_navigation_intent(url, false);
1697
1754
  const nav =
1698
1755
  type === 'enter'
1699
1756
  ? create_navigation(current, intent, url, type)
@@ -1709,7 +1766,8 @@ async function navigate({
1709
1766
 
1710
1767
  if (!nav) {
1711
1768
  block();
1712
- if (token === nav_token) token = prev_token;
1769
+ if (navigation_token === nav_token) navigation_token = prev_token;
1770
+ if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
1713
1771
  return;
1714
1772
  }
1715
1773
 
@@ -1747,7 +1805,6 @@ async function navigate({
1747
1805
  route: { id: null }
1748
1806
  }
1749
1807
  ),
1750
- 404,
1751
1808
  replace_state
1752
1809
  );
1753
1810
  } else {
@@ -1762,7 +1819,6 @@ async function navigate({
1762
1819
  params: {},
1763
1820
  route: { id: null }
1764
1821
  }),
1765
- 404,
1766
1822
  replace_state
1767
1823
  );
1768
1824
  }
@@ -1773,11 +1829,13 @@ async function navigate({
1773
1829
  url = intent?.url || url;
1774
1830
 
1775
1831
  // abort if user navigated during update
1776
- if (token !== nav_token) {
1832
+ if (navigation_token !== nav_token) {
1777
1833
  nav.reject(new Error('navigation aborted'));
1778
- return false;
1834
+ return;
1779
1835
  }
1780
1836
 
1837
+ if (!navigation_result) return;
1838
+
1781
1839
  if (navigation_result.type === 'redirect') {
1782
1840
  // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1783
1841
  if (redirect_count < 20) {
@@ -1798,7 +1856,6 @@ async function navigate({
1798
1856
  }
1799
1857
 
1800
1858
  navigation_result = await load_root_error_page({
1801
- status: 500,
1802
1859
  error: await handle_error(new Error('Redirect loop'), {
1803
1860
  url,
1804
1861
  params: {},
@@ -1807,12 +1864,14 @@ async function navigate({
1807
1864
  url,
1808
1865
  route: { id: null }
1809
1866
  });
1867
+
1868
+ if (!navigation_result) return;
1810
1869
  } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1811
1870
  const updated = await stores.updated.check();
1812
1871
  if (updated) {
1813
1872
  // Before reloading, try to update the service worker if it exists
1814
1873
  await update_service_worker();
1815
- await native_navigation(url, replace_state);
1874
+ return await native_navigation(url, replace_state);
1816
1875
  }
1817
1876
  }
1818
1877
 
@@ -1907,6 +1966,16 @@ async function navigate({
1907
1966
  navigation_result.props.page.url = url;
1908
1967
  }
1909
1968
 
1969
+ // Remove focus before updating the component tree, so that blur/focusout
1970
+ // handlers fire while the old component's data is still valid (#14575)
1971
+ if (
1972
+ !keepfocus &&
1973
+ document.activeElement instanceof HTMLElement &&
1974
+ document.activeElement !== document.body
1975
+ ) {
1976
+ document.activeElement.blur();
1977
+ }
1978
+
1910
1979
  const fork = load_cache_fork && (await load_cache_fork);
1911
1980
 
1912
1981
  if (fork) {
@@ -1938,10 +2007,10 @@ async function navigate({
1938
2007
  await svelte.tick();
1939
2008
  await svelte.tick();
1940
2009
 
1941
- if (token !== nav_token) {
2010
+ if (navigation_token !== nav_token) {
1942
2011
  // a new navigation happened while we were waiting for the DOM to update, so abort
1943
2012
  nav.reject(new Error('navigation aborted'));
1944
- return false;
2013
+ return;
1945
2014
  }
1946
2015
 
1947
2016
  // Check for async rendering error
@@ -1985,10 +2054,6 @@ async function navigate({
1985
2054
 
1986
2055
  is_navigating = false;
1987
2056
 
1988
- if (type === 'popstate') {
1989
- restore_snapshot(current_navigation_index);
1990
- }
1991
-
1992
2057
  nav.fulfil(undefined);
1993
2058
 
1994
2059
  // Update to.scroll to the actual scroll position after navigation completed
@@ -2000,6 +2065,10 @@ async function navigate({
2000
2065
  fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
2001
2066
  );
2002
2067
 
2068
+ if (type === 'popstate') {
2069
+ restore_snapshot(current_navigation_index);
2070
+ }
2071
+
2003
2072
  stores.navigating.set((navigating.current = null));
2004
2073
 
2005
2074
  updating = false;
@@ -2010,23 +2079,21 @@ async function navigate({
2010
2079
  * @param {URL} url
2011
2080
  * @param {{ id: string | null }} route
2012
2081
  * @param {App.Error} error
2013
- * @param {number} status
2014
2082
  * @param {boolean} [replace_state]
2015
- * @returns {Promise<import('./types.js').NavigationFinished>}
2083
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>}
2016
2084
  */
2017
- async function server_fallback(url, route, error, status, replace_state) {
2085
+ async function server_fallback(url, route, error, replace_state) {
2018
2086
  if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
2019
2087
  // We would reload the same page we're currently on, which isn't hydrated,
2020
2088
  // which means no SSR, which means we would end up in an endless loop
2021
2089
  return await load_root_error_page({
2022
- status,
2023
2090
  error,
2024
2091
  url,
2025
2092
  route
2026
2093
  });
2027
2094
  }
2028
2095
 
2029
- if (DEV && status !== 404) {
2096
+ if (DEV && error.status !== 404) {
2030
2097
  console.error(
2031
2098
  'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
2032
2099
  );
@@ -2165,9 +2232,9 @@ function setup_preload() {
2165
2232
  /**
2166
2233
  * @param {unknown} error
2167
2234
  * @param {import('@sveltejs/kit').NavigationEvent} event
2168
- * @returns {import('types').MaybePromise<App.Error>}
2235
+ * @returns {Promise<App.Error>}
2169
2236
  */
2170
- function handle_error(error, event) {
2237
+ export async function handle_error(error, event) {
2171
2238
  if (error instanceof HttpError) {
2172
2239
  return error.body;
2173
2240
  }
@@ -2179,10 +2246,9 @@ function handle_error(error, event) {
2179
2246
 
2180
2247
  const status = get_status(error);
2181
2248
  const message = get_message(error);
2249
+ const app_error = (await app.hooks.handleError({ error, event, status, message })) ?? { message };
2182
2250
 
2183
- return (
2184
- app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
2185
- );
2251
+ return { ...app_error, status: get_status(app_error, error) };
2186
2252
  }
2187
2253
 
2188
2254
  /**
@@ -2266,9 +2332,11 @@ export function disableScrollHandling() {
2266
2332
  * Allows you to navigate programmatically to a given route, with options such as keeping the current element focused.
2267
2333
  * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
2268
2334
  *
2269
- * For external URLs, use `window.location = url` instead of calling `goto(url)`.
2335
+ * `goto` is intended for navigations to routes that belong to the app.
2336
+ * If the URL does not resolve to a route within the app, the returned promise will reject.
2337
+ * For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
2270
2338
  *
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.
2339
+ * @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
2340
  * @param {Object} [opts] Options related to the navigation
2273
2341
  * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
2274
2342
  * @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 +2346,7 @@ export function disableScrollHandling() {
2278
2346
  * @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
2279
2347
  * @returns {Promise<void>}
2280
2348
  */
2281
- export function goto(url, opts = {}) {
2349
+ export async function goto(url, opts = {}) {
2282
2350
  if (!BROWSER) {
2283
2351
  throw new Error('Cannot call goto(...) on the server');
2284
2352
  }
@@ -2286,16 +2354,24 @@ export function goto(url, opts = {}) {
2286
2354
  url = new URL(resolve_url(url));
2287
2355
 
2288
2356
  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
- )
2357
+ throw new Error(
2358
+ DEV
2359
+ ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2360
+ : 'goto: invalid URL'
2295
2361
  );
2296
2362
  }
2297
2363
 
2298
- return _goto(url, opts, 0);
2364
+ const intent = await get_navigation_intent(url, false);
2365
+
2366
+ if (!intent) {
2367
+ throw new Error(
2368
+ DEV
2369
+ ? `Cannot use \`goto\` with a URL that does not resolve to a route within the app. Use \`window.location = "${url}"\` instead`
2370
+ : 'goto: invalid URL'
2371
+ );
2372
+ }
2373
+
2374
+ return _goto(url, opts, 0, {}, intent);
2299
2375
  }
2300
2376
 
2301
2377
  /**
@@ -2376,7 +2452,7 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
2376
2452
  * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
2377
2453
  *
2378
2454
  * @param {string} href Page to preload
2379
- * @returns {Promise<{ type: 'loaded'; status: number; data: Record<string, any> } | { type: 'redirect'; location: string }>}
2455
+ * @returns {Promise<({ type: 'loaded'; data: Record<string, any> } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>}
2380
2456
  */
2381
2457
  export async function preloadData(href) {
2382
2458
  if (!BROWSER) {
@@ -2394,11 +2470,21 @@ export async function preloadData(href) {
2394
2470
  if (result.type === 'redirect') {
2395
2471
  return {
2396
2472
  type: result.type,
2473
+ status: result.status,
2397
2474
  location: result.location
2398
2475
  };
2399
2476
  }
2400
2477
 
2401
2478
  const { status, data } = result.props.page ?? page;
2479
+
2480
+ if (result.type === 'loaded' && result.state.error) {
2481
+ return {
2482
+ type: 'error',
2483
+ status,
2484
+ error: result.state.error
2485
+ };
2486
+ }
2487
+
2402
2488
  return { type: result.type, status, data };
2403
2489
  }
2404
2490
 
@@ -2419,7 +2505,9 @@ export async function preloadCode(pathname) {
2419
2505
  throw new Error('Cannot call preloadCode(...) on the server');
2420
2506
  }
2421
2507
 
2422
- const url = new URL(pathname, current.url);
2508
+ // `current.url` is null until the first navigation/hydration completes, so fall back
2509
+ // to `location` to support calling `preloadCode` during initial page load (#13297)
2510
+ const url = new URL(pathname, current.url ?? location.href);
2423
2511
 
2424
2512
  if (DEV) {
2425
2513
  if (!pathname.startsWith('/')) {
@@ -2547,7 +2635,7 @@ export async function applyAction(result) {
2547
2635
  }
2548
2636
 
2549
2637
  if (result.type === 'error') {
2550
- await set_nearest_error_page(result.error, result.status);
2638
+ await set_nearest_error_page(result.error);
2551
2639
  } else if (result.type === 'redirect') {
2552
2640
  await _goto(result.location, { invalidateAll: true }, 0);
2553
2641
  } else {
@@ -2574,9 +2662,8 @@ export async function applyAction(result) {
2574
2662
 
2575
2663
  /**
2576
2664
  * @param {App.Error} error
2577
- * @param {number} status
2578
2665
  */
2579
- export async function set_nearest_error_page(error, status = 500) {
2666
+ export async function set_nearest_error_page(error) {
2580
2667
  const url = new URL(location.href);
2581
2668
 
2582
2669
  const { branch, route } = current;
@@ -2588,7 +2675,6 @@ export async function set_nearest_error_page(error, status = 500) {
2588
2675
  url,
2589
2676
  params: current.params,
2590
2677
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
2591
- status,
2592
2678
  error,
2593
2679
  // do not set errors, we haven't changed the page so the previous ones are still current
2594
2680
  route
@@ -2823,7 +2909,7 @@ function _start_router() {
2823
2909
 
2824
2910
  if (event.state?.[HISTORY_INDEX]) {
2825
2911
  const history_index = event.state[HISTORY_INDEX];
2826
- token = {};
2912
+ navigation_token = invalidation_token = {};
2827
2913
 
2828
2914
  // if a popstate-driven navigation is cancelled, we need to counteract it
2829
2915
  // with history.go, which means we end up back here, hence this check
@@ -2872,7 +2958,7 @@ function _start_router() {
2872
2958
  block: () => {
2873
2959
  history.go(-delta);
2874
2960
  },
2875
- nav_token: token,
2961
+ nav_token: navigation_token,
2876
2962
  event
2877
2963
  });
2878
2964
  } else {
@@ -2941,10 +3027,11 @@ function _start_router() {
2941
3027
  /**
2942
3028
  * @param {HTMLElement} target
2943
3029
  * @param {import('./types.js').HydrateOptions} opts
3030
+ * @returns {Promise<void>}
2944
3031
  */
2945
3032
  async function _hydrate(
2946
3033
  target,
2947
- { status = 200, error, node_ids, params, route, server_route, data: server_data_nodes, form }
3034
+ { status, error, node_ids, params, route, server_route, data: server_data_nodes, form }
2948
3035
  ) {
2949
3036
  hydrated = true;
2950
3037
 
@@ -3027,13 +3114,13 @@ async function _hydrate(
3027
3114
  if (error instanceof Redirect) {
3028
3115
  // this is a real edge case — `load` would need to return
3029
3116
  // a redirect but only in the browser
3030
- await native_navigation(new URL(error.location, location.href));
3031
- return;
3117
+ return await native_navigation(new URL(error.location, location.href));
3032
3118
  }
3033
3119
 
3120
+ const handled_error = await handle_error(error, { url, params, route });
3121
+
3034
3122
  result = await load_root_error_page({
3035
- status: get_status(error),
3036
- error: await handle_error(error, { url, params, route }),
3123
+ error: handled_error,
3037
3124
  url,
3038
3125
  route
3039
3126
  });
@@ -3042,6 +3129,10 @@ async function _hydrate(
3042
3129
  hydrate = false;
3043
3130
  }
3044
3131
 
3132
+ // Exit early when we encounter a redirect while loading the root error page.
3133
+ // In this case, `initialize` will be called later on
3134
+ if (!result) return;
3135
+
3045
3136
  if (result.props.page) {
3046
3137
  result.props.page.state = {};
3047
3138
  }