@sveltejs/kit 3.0.0-next.3 → 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 +190 -95
  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 +26 -10
  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,12 +575,17 @@ 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;
555
582
  query_keys = new Set();
556
583
  for (const [id, entries] of query_map) {
557
- for (const payload of entries.keys()) {
584
+ for (const [payload, entry] of entries) {
585
+ // don't refresh yet, as some queries will be unrendered,
586
+ // but clear caches so that newly rendered queries
587
+ // don't use stale data. TODO same for `live_query_map`
588
+ entry.resource?.reset();
558
589
  query_keys.add(create_remote_key(id, payload));
559
590
  }
560
591
  }
@@ -582,7 +613,7 @@ export async function _goto(url, options, redirect_count, nav_token) {
582
613
  for (const [id, entries] of query_map) {
583
614
  for (const [payload, { resource }] of entries) {
584
615
  if (query_keys?.has(create_remote_key(id, payload))) {
585
- void resource.refresh();
616
+ void resource.start();
586
617
  }
587
618
  }
588
619
  }
@@ -702,7 +733,7 @@ async function initialize(result, target, hydrate) {
702
733
  transformError: __SVELTEKIT_EXPERIMENTAL_USE_TRANSFORM_ERROR__
703
734
  ? /** @param {unknown} e */ async (e) => {
704
735
  const error = await handle_error(e, current.nav);
705
- rendering_error = { error, status: get_status(e) };
736
+ rendering_error = { error, status: error.status };
706
737
  page.error = error;
707
738
  page.status = rendering_error.status;
708
739
  return error;
@@ -714,8 +745,6 @@ async function initialize(result, target, hydrate) {
714
745
  // which causes component script blocks to run asynchronously
715
746
  void (await Promise.resolve());
716
747
 
717
- restore_snapshot(current_navigation_index);
718
-
719
748
  if (hydrate) {
720
749
  /** @type {import('@sveltejs/kit').AfterNavigate} */
721
750
  const navigation = {
@@ -732,6 +761,8 @@ async function initialize(result, target, hydrate) {
732
761
  after_navigate_callbacks.forEach((fn) => fn(navigation));
733
762
  }
734
763
 
764
+ restore_snapshot(current_navigation_index);
765
+
735
766
  started = true;
736
767
  }
737
768
 
@@ -742,7 +773,7 @@ async function initialize(result, target, hydrate) {
742
773
  * params: Record<string, string>;
743
774
  * branch: Array<import('./types.js').BranchNode | undefined>;
744
775
  * errors?: Array<import('types').CSRPageNodeLoader | undefined>;
745
- * status: number;
776
+ * status?: number;
746
777
  * error: App.Error | null;
747
778
  * route: import('types').CSRRoute | null;
748
779
  * form?: Record<string, any> | null;
@@ -859,7 +890,7 @@ async function get_navigation_result_from_branch({
859
890
  id: route?.id ?? null
860
891
  },
861
892
  state: {},
862
- status,
893
+ status: status ?? error?.status ?? 200,
863
894
  url: new URL(url),
864
895
  form: form ?? null,
865
896
  // The whole page store is updated, but this way the object reference stays the same
@@ -1146,6 +1177,10 @@ function diff_search_params(old_url, new_url) {
1146
1177
  * @returns {import('./types.js').NavigationFinished}
1147
1178
  */
1148
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;
1149
1184
  return {
1150
1185
  type: 'loaded',
1151
1186
  state: {
@@ -1156,16 +1191,26 @@ function preload_error({ error, url, route, params }) {
1156
1191
  branch: []
1157
1192
  },
1158
1193
  props: {
1159
- page: clone_page(page),
1194
+ page: new_page,
1160
1195
  constructors: []
1161
1196
  }
1162
1197
  };
1163
1198
  }
1164
1199
 
1165
1200
  /**
1166
- * @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
1167
1208
  * @returns {Promise<import('./types.js').NavigationResult>}
1168
1209
  */
1210
+ /**
1211
+ * @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
1212
+ * @returns {Promise<import('./types.js').NavigationResult | undefined>}
1213
+ */
1169
1214
  async function load_route({ id, invalidating, url, params, route, preload }) {
1170
1215
  if (load_cache?.id === id) {
1171
1216
  // the preload becomes the real navigation
@@ -1221,12 +1266,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1221
1266
  } catch (error) {
1222
1267
  const handled_error = await handle_error(error, { url, params, route: { id } });
1223
1268
 
1224
- if (preload_tokens.has(preload)) {
1269
+ if (preload && preload_tokens.has(preload)) {
1225
1270
  return preload_error({ error: handled_error, url, params, route });
1226
1271
  }
1227
1272
 
1228
1273
  return load_root_error_page({
1229
- status: get_status(error),
1230
1274
  error: handled_error,
1231
1275
  url,
1232
1276
  route
@@ -1269,7 +1313,7 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1269
1313
 
1270
1314
  if (server_data_node?.type === 'error') {
1271
1315
  // rethrow and catch below
1272
- throw server_data_node;
1316
+ throw new HttpError(server_data_node.error.status, server_data_node.error);
1273
1317
  }
1274
1318
 
1275
1319
  return load_node({
@@ -1307,27 +1351,27 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1307
1351
  if (err instanceof Redirect) {
1308
1352
  return {
1309
1353
  type: 'redirect',
1354
+ status: err.status,
1310
1355
  location: err.location
1311
1356
  };
1312
1357
  }
1313
1358
 
1314
- 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 } });
1315
1361
  return preload_error({
1316
- error: await handle_error(err, { params, url, route: { id: route.id } }),
1362
+ error,
1317
1363
  url,
1318
1364
  params,
1319
1365
  route
1320
1366
  });
1321
1367
  }
1322
1368
 
1323
- let status = get_status(err);
1324
1369
  /** @type {App.Error} */
1325
1370
  let error;
1326
1371
 
1327
1372
  if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
1328
1373
  // this is the server error rethrown above, reconstruct but don't invoke
1329
1374
  // the client error handler; it should've already been handled on the server
1330
- status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
1331
1375
  error = /** @type {import('types').ServerErrorNode} */ (err).error;
1332
1376
  } else if (err instanceof HttpError) {
1333
1377
  error = err.body;
@@ -1350,12 +1394,11 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1350
1394
  params,
1351
1395
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
1352
1396
  errors,
1353
- status,
1354
1397
  error,
1355
1398
  route
1356
1399
  });
1357
1400
  } else {
1358
- return await server_fallback(url, { id: route.id }, error, status);
1401
+ return await server_fallback(url, { id: route.id }, error);
1359
1402
  }
1360
1403
  }
1361
1404
  } else {
@@ -1370,7 +1413,6 @@ async function load_route({ id, invalidating, url, params, route, preload }) {
1370
1413
  params,
1371
1414
  branch,
1372
1415
  errors,
1373
- status: 200,
1374
1416
  error: null,
1375
1417
  route,
1376
1418
  // Reset `form` on navigation, but not invalidation
@@ -1409,14 +1451,13 @@ async function load_nearest_error_page(i, branch, errors) {
1409
1451
 
1410
1452
  /**
1411
1453
  * @param {{
1412
- * status: number;
1413
1454
  * error: App.Error;
1414
1455
  * url: URL;
1415
1456
  * route: { id: string | null }
1416
1457
  * }} opts
1417
- * @returns {Promise<import('./types.js').NavigationFinished>}
1458
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>} returns `undefined` in case of a redirect
1418
1459
  */
1419
- async function load_root_error_page({ status, error, url, route }) {
1460
+ async function load_root_error_page({ error, url, route }) {
1420
1461
  /** @type {Record<string, string>} */
1421
1462
  const params = {}; // error page does not have params
1422
1463
 
@@ -1440,11 +1481,15 @@ async function load_root_error_page({ status, error, url, route }) {
1440
1481
  }
1441
1482
 
1442
1483
  server_data_node = server_data.nodes[0] ?? null;
1443
- } catch {
1484
+ } catch (e) {
1444
1485
  // at this point we have no choice but to fall back to the server, if it wouldn't
1445
- // bring us right back here, turning this into an endless loop
1446
- if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
1447
- 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);
1448
1493
  }
1449
1494
  }
1450
1495
  }
@@ -1473,18 +1518,30 @@ async function load_root_error_page({ status, error, url, route }) {
1473
1518
  url,
1474
1519
  params,
1475
1520
  branch: [root_layout, root_error],
1476
- status,
1477
1521
  error,
1478
1522
  errors: [],
1479
1523
  route: null
1480
1524
  });
1481
1525
  } catch (error) {
1526
+ // client-side navigation if the root layout loader throws a redirect while
1527
+ // rendering the default error page
1482
1528
  if (error instanceof Redirect) {
1483
- // @ts-expect-error TODO investigate this
1484
- return _goto(new URL(error.location, location.href), {}, 0);
1529
+ await _goto(new URL(error.location, location.href), {}, 0);
1530
+ return;
1485
1531
  }
1486
1532
 
1487
- // 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
+
1488
1545
  throw error;
1489
1546
  }
1490
1547
  }
@@ -1669,8 +1726,10 @@ function _before_navigate({ url, type, intent, delta, event, scroll }) {
1669
1726
  * nav_token?: {};
1670
1727
  * accept?: () => void;
1671
1728
  * block?: () => void;
1672
- * event?: Event
1729
+ * event?: Event;
1730
+ * intent?: NavigationIntent | undefined
1673
1731
  * }} opts
1732
+ * @returns {Promise<void>}
1674
1733
  */
1675
1734
  async function navigate({
1676
1735
  type,
@@ -1684,12 +1743,14 @@ async function navigate({
1684
1743
  nav_token = {},
1685
1744
  accept = noop,
1686
1745
  block = noop,
1687
- event
1746
+ event,
1747
+ intent
1688
1748
  }) {
1689
- const prev_token = token;
1690
- token = nav_token;
1749
+ const prev_token = navigation_token;
1750
+ const prev_invalidation_token = invalidation_token;
1751
+ navigation_token = invalidation_token = nav_token;
1691
1752
 
1692
- const intent = await get_navigation_intent(url, false);
1753
+ intent ??= await get_navigation_intent(url, false);
1693
1754
  const nav =
1694
1755
  type === 'enter'
1695
1756
  ? create_navigation(current, intent, url, type)
@@ -1705,7 +1766,8 @@ async function navigate({
1705
1766
 
1706
1767
  if (!nav) {
1707
1768
  block();
1708
- 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;
1709
1771
  return;
1710
1772
  }
1711
1773
 
@@ -1743,7 +1805,6 @@ async function navigate({
1743
1805
  route: { id: null }
1744
1806
  }
1745
1807
  ),
1746
- 404,
1747
1808
  replace_state
1748
1809
  );
1749
1810
  } else {
@@ -1758,7 +1819,6 @@ async function navigate({
1758
1819
  params: {},
1759
1820
  route: { id: null }
1760
1821
  }),
1761
- 404,
1762
1822
  replace_state
1763
1823
  );
1764
1824
  }
@@ -1769,11 +1829,13 @@ async function navigate({
1769
1829
  url = intent?.url || url;
1770
1830
 
1771
1831
  // abort if user navigated during update
1772
- if (token !== nav_token) {
1832
+ if (navigation_token !== nav_token) {
1773
1833
  nav.reject(new Error('navigation aborted'));
1774
- return false;
1834
+ return;
1775
1835
  }
1776
1836
 
1837
+ if (!navigation_result) return;
1838
+
1777
1839
  if (navigation_result.type === 'redirect') {
1778
1840
  // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1779
1841
  if (redirect_count < 20) {
@@ -1794,7 +1856,6 @@ async function navigate({
1794
1856
  }
1795
1857
 
1796
1858
  navigation_result = await load_root_error_page({
1797
- status: 500,
1798
1859
  error: await handle_error(new Error('Redirect loop'), {
1799
1860
  url,
1800
1861
  params: {},
@@ -1803,12 +1864,14 @@ async function navigate({
1803
1864
  url,
1804
1865
  route: { id: null }
1805
1866
  });
1867
+
1868
+ if (!navigation_result) return;
1806
1869
  } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1807
1870
  const updated = await stores.updated.check();
1808
1871
  if (updated) {
1809
1872
  // Before reloading, try to update the service worker if it exists
1810
1873
  await update_service_worker();
1811
- await native_navigation(url, replace_state);
1874
+ return await native_navigation(url, replace_state);
1812
1875
  }
1813
1876
  }
1814
1877
 
@@ -1903,6 +1966,16 @@ async function navigate({
1903
1966
  navigation_result.props.page.url = url;
1904
1967
  }
1905
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
+
1906
1979
  const fork = load_cache_fork && (await load_cache_fork);
1907
1980
 
1908
1981
  if (fork) {
@@ -1934,10 +2007,10 @@ async function navigate({
1934
2007
  await svelte.tick();
1935
2008
  await svelte.tick();
1936
2009
 
1937
- if (token !== nav_token) {
2010
+ if (navigation_token !== nav_token) {
1938
2011
  // a new navigation happened while we were waiting for the DOM to update, so abort
1939
2012
  nav.reject(new Error('navigation aborted'));
1940
- return false;
2013
+ return;
1941
2014
  }
1942
2015
 
1943
2016
  // Check for async rendering error
@@ -1981,10 +2054,6 @@ async function navigate({
1981
2054
 
1982
2055
  is_navigating = false;
1983
2056
 
1984
- if (type === 'popstate') {
1985
- restore_snapshot(current_navigation_index);
1986
- }
1987
-
1988
2057
  nav.fulfil(undefined);
1989
2058
 
1990
2059
  // Update to.scroll to the actual scroll position after navigation completed
@@ -1996,6 +2065,10 @@ async function navigate({
1996
2065
  fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
1997
2066
  );
1998
2067
 
2068
+ if (type === 'popstate') {
2069
+ restore_snapshot(current_navigation_index);
2070
+ }
2071
+
1999
2072
  stores.navigating.set((navigating.current = null));
2000
2073
 
2001
2074
  updating = false;
@@ -2006,23 +2079,21 @@ async function navigate({
2006
2079
  * @param {URL} url
2007
2080
  * @param {{ id: string | null }} route
2008
2081
  * @param {App.Error} error
2009
- * @param {number} status
2010
2082
  * @param {boolean} [replace_state]
2011
- * @returns {Promise<import('./types.js').NavigationFinished>}
2083
+ * @returns {Promise<import('./types.js').NavigationFinished | undefined>}
2012
2084
  */
2013
- async function server_fallback(url, route, error, status, replace_state) {
2085
+ async function server_fallback(url, route, error, replace_state) {
2014
2086
  if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
2015
2087
  // We would reload the same page we're currently on, which isn't hydrated,
2016
2088
  // which means no SSR, which means we would end up in an endless loop
2017
2089
  return await load_root_error_page({
2018
- status,
2019
2090
  error,
2020
2091
  url,
2021
2092
  route
2022
2093
  });
2023
2094
  }
2024
2095
 
2025
- if (DEV && status !== 404) {
2096
+ if (DEV && error.status !== 404) {
2026
2097
  console.error(
2027
2098
  'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
2028
2099
  );
@@ -2161,9 +2232,9 @@ function setup_preload() {
2161
2232
  /**
2162
2233
  * @param {unknown} error
2163
2234
  * @param {import('@sveltejs/kit').NavigationEvent} event
2164
- * @returns {import('types').MaybePromise<App.Error>}
2235
+ * @returns {Promise<App.Error>}
2165
2236
  */
2166
- function handle_error(error, event) {
2237
+ export async function handle_error(error, event) {
2167
2238
  if (error instanceof HttpError) {
2168
2239
  return error.body;
2169
2240
  }
@@ -2175,10 +2246,9 @@ function handle_error(error, event) {
2175
2246
 
2176
2247
  const status = get_status(error);
2177
2248
  const message = get_message(error);
2249
+ const app_error = (await app.hooks.handleError({ error, event, status, message })) ?? { message };
2178
2250
 
2179
- return (
2180
- app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
2181
- );
2251
+ return { ...app_error, status: get_status(app_error, error) };
2182
2252
  }
2183
2253
 
2184
2254
  /**
@@ -2262,9 +2332,11 @@ export function disableScrollHandling() {
2262
2332
  * Allows you to navigate programmatically to a given route, with options such as keeping the current element focused.
2263
2333
  * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
2264
2334
  *
2265
- * 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)`.
2266
2338
  *
2267
- * @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.
2268
2340
  * @param {Object} [opts] Options related to the navigation
2269
2341
  * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
2270
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
@@ -2274,7 +2346,7 @@ export function disableScrollHandling() {
2274
2346
  * @param {App.PageState} [opts.state] An optional object that will be available as `page.state`
2275
2347
  * @returns {Promise<void>}
2276
2348
  */
2277
- export function goto(url, opts = {}) {
2349
+ export async function goto(url, opts = {}) {
2278
2350
  if (!BROWSER) {
2279
2351
  throw new Error('Cannot call goto(...) on the server');
2280
2352
  }
@@ -2282,16 +2354,24 @@ export function goto(url, opts = {}) {
2282
2354
  url = new URL(resolve_url(url));
2283
2355
 
2284
2356
  if (url.origin !== origin) {
2285
- return Promise.reject(
2286
- new Error(
2287
- DEV
2288
- ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2289
- : 'goto: invalid URL'
2290
- )
2357
+ throw new Error(
2358
+ DEV
2359
+ ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
2360
+ : 'goto: invalid URL'
2291
2361
  );
2292
2362
  }
2293
2363
 
2294
- 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);
2295
2375
  }
2296
2376
 
2297
2377
  /**
@@ -2372,7 +2452,7 @@ export function refreshAll({ includeLoadFunctions = true } = {}) {
2372
2452
  * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
2373
2453
  *
2374
2454
  * @param {string} href Page to preload
2375
- * @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; }>}
2376
2456
  */
2377
2457
  export async function preloadData(href) {
2378
2458
  if (!BROWSER) {
@@ -2390,11 +2470,21 @@ export async function preloadData(href) {
2390
2470
  if (result.type === 'redirect') {
2391
2471
  return {
2392
2472
  type: result.type,
2473
+ status: result.status,
2393
2474
  location: result.location
2394
2475
  };
2395
2476
  }
2396
2477
 
2397
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
+
2398
2488
  return { type: result.type, status, data };
2399
2489
  }
2400
2490
 
@@ -2415,7 +2505,9 @@ export async function preloadCode(pathname) {
2415
2505
  throw new Error('Cannot call preloadCode(...) on the server');
2416
2506
  }
2417
2507
 
2418
- 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);
2419
2511
 
2420
2512
  if (DEV) {
2421
2513
  if (!pathname.startsWith('/')) {
@@ -2543,7 +2635,7 @@ export async function applyAction(result) {
2543
2635
  }
2544
2636
 
2545
2637
  if (result.type === 'error') {
2546
- await set_nearest_error_page(result.error, result.status);
2638
+ await set_nearest_error_page(result.error);
2547
2639
  } else if (result.type === 'redirect') {
2548
2640
  await _goto(result.location, { invalidateAll: true }, 0);
2549
2641
  } else {
@@ -2570,9 +2662,8 @@ export async function applyAction(result) {
2570
2662
 
2571
2663
  /**
2572
2664
  * @param {App.Error} error
2573
- * @param {number} status
2574
2665
  */
2575
- export async function set_nearest_error_page(error, status = 500) {
2666
+ export async function set_nearest_error_page(error) {
2576
2667
  const url = new URL(location.href);
2577
2668
 
2578
2669
  const { branch, route } = current;
@@ -2584,7 +2675,6 @@ export async function set_nearest_error_page(error, status = 500) {
2584
2675
  url,
2585
2676
  params: current.params,
2586
2677
  branch: branch.slice(0, error_load.idx).concat(error_load.node),
2587
- status,
2588
2678
  error,
2589
2679
  // do not set errors, we haven't changed the page so the previous ones are still current
2590
2680
  route
@@ -2819,7 +2909,7 @@ function _start_router() {
2819
2909
 
2820
2910
  if (event.state?.[HISTORY_INDEX]) {
2821
2911
  const history_index = event.state[HISTORY_INDEX];
2822
- token = {};
2912
+ navigation_token = invalidation_token = {};
2823
2913
 
2824
2914
  // if a popstate-driven navigation is cancelled, we need to counteract it
2825
2915
  // with history.go, which means we end up back here, hence this check
@@ -2868,7 +2958,7 @@ function _start_router() {
2868
2958
  block: () => {
2869
2959
  history.go(-delta);
2870
2960
  },
2871
- nav_token: token,
2961
+ nav_token: navigation_token,
2872
2962
  event
2873
2963
  });
2874
2964
  } else {
@@ -2937,10 +3027,11 @@ function _start_router() {
2937
3027
  /**
2938
3028
  * @param {HTMLElement} target
2939
3029
  * @param {import('./types.js').HydrateOptions} opts
3030
+ * @returns {Promise<void>}
2940
3031
  */
2941
3032
  async function _hydrate(
2942
3033
  target,
2943
- { 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 }
2944
3035
  ) {
2945
3036
  hydrated = true;
2946
3037
 
@@ -3023,13 +3114,13 @@ async function _hydrate(
3023
3114
  if (error instanceof Redirect) {
3024
3115
  // this is a real edge case — `load` would need to return
3025
3116
  // a redirect but only in the browser
3026
- await native_navigation(new URL(error.location, location.href));
3027
- return;
3117
+ return await native_navigation(new URL(error.location, location.href));
3028
3118
  }
3029
3119
 
3120
+ const handled_error = await handle_error(error, { url, params, route });
3121
+
3030
3122
  result = await load_root_error_page({
3031
- status: get_status(error),
3032
- error: await handle_error(error, { url, params, route }),
3123
+ error: handled_error,
3033
3124
  url,
3034
3125
  route
3035
3126
  });
@@ -3038,6 +3129,10 @@ async function _hydrate(
3038
3129
  hydrate = false;
3039
3130
  }
3040
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
+
3041
3136
  if (result.props.page) {
3042
3137
  result.props.page.state = {};
3043
3138
  }