@sveltejs/kit 2.0.8 → 2.1.0

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.
@@ -1,4 +1,4 @@
1
- import { DEV } from 'esm-env';
1
+ import { BROWSER, DEV } from 'esm-env';
2
2
  import { onMount, tick } from 'svelte';
3
3
  import {
4
4
  add_data_suffix,
@@ -24,9 +24,10 @@ import {
24
24
  get_router_options,
25
25
  is_external_url,
26
26
  origin,
27
- scroll_state
27
+ scroll_state,
28
+ notifiable_store,
29
+ create_updated_store
28
30
  } from './utils.js';
29
-
30
31
  import { base } from '__sveltekit/paths';
31
32
  import * as devalue from 'devalue';
32
33
  import {
@@ -42,39 +43,31 @@ import { validate_page_exports } from '../../utils/exports.js';
42
43
  import { compact } from '../../utils/array.js';
43
44
  import { HttpError, Redirect, SvelteKitError } from '../control.js';
44
45
  import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js';
45
- import { stores } from './singletons.js';
46
46
  import { get_message, get_status } from '../../utils/error.js';
47
+ import { writable } from 'svelte/store';
47
48
 
48
49
  let errored = false;
49
50
 
50
- /** @typedef {{ x: number; y: number }} ScrollPosition */
51
-
52
51
  // We track the scroll position associated with each history entry in sessionStorage,
53
52
  // rather than on history.state itself, because when navigation is driven by
54
53
  // popstate it's too late to update the scroll position associated with the
55
54
  // state we're navigating from
56
55
  /**
57
56
  * history index -> { x, y }
58
- * @type {Record<number, ScrollPosition>}
57
+ * @type {Record<number, { x: number; y: number }>}
59
58
  */
60
59
  const scroll_positions = storage.get(SCROLL_KEY) ?? {};
61
60
 
62
- /**
63
- * history index -> any
64
- * @type {Record<string, Record<string, any>>}
65
- */
66
- const states = storage.get(STATES_KEY, devalue.parse) ?? {};
67
-
68
61
  /**
69
62
  * navigation index -> any
70
63
  * @type {Record<string, any[]>}
71
64
  */
72
65
  const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
73
66
 
74
- const original_push_state = history.pushState;
75
- const original_replace_state = history.replaceState;
67
+ const original_push_state = BROWSER ? history.pushState : () => {};
68
+ const original_replace_state = BROWSER ? history.replaceState : () => {};
76
69
 
77
- if (DEV) {
70
+ if (DEV && BROWSER) {
78
71
  let warned = false;
79
72
 
80
73
  const warn = () => {
@@ -97,6 +90,15 @@ if (DEV) {
97
90
  };
98
91
  }
99
92
 
93
+ export const stores = {
94
+ url: /* @__PURE__ */ notifiable_store({}),
95
+ page: /* @__PURE__ */ notifiable_store({}),
96
+ navigating: /* @__PURE__ */ writable(
97
+ /** @type {import('@sveltejs/kit').Navigation | null} */ (null)
98
+ ),
99
+ updated: /* @__PURE__ */ create_updated_store()
100
+ };
101
+
100
102
  /** @param {number} index */
101
103
  function update_scroll_positions(index) {
102
104
  scroll_positions[index] = scroll_state();
@@ -135,78 +137,106 @@ function native_navigation(url) {
135
137
 
136
138
  function noop() {}
137
139
 
140
+ /** @type {import('types').CSRRoute[]} */
141
+ let routes;
142
+ /** @type {import('types').CSRPageNodeLoader} */
143
+ let default_layout_loader;
144
+ /** @type {import('types').CSRPageNodeLoader} */
145
+ let default_error_loader;
146
+ /** @type {HTMLElement} */
147
+ let container;
148
+ /** @type {HTMLElement} */
149
+ let target;
150
+ /** @type {import('./types.js').SvelteKitApp} */
151
+ let app;
152
+
153
+ /** @type {Array<((url: URL) => boolean)>} */
154
+ const invalidated = [];
155
+
138
156
  /**
139
- * @param {import('./types.js').SvelteKitApp} app
140
- * @param {HTMLElement} target
141
- * @returns {import('./types.js').Client}
157
+ * An array of the `+layout.svelte` and `+page.svelte` component instances
158
+ * that currently live on the page — used for capturing and restoring snapshots.
159
+ * It's updated/manipulated through `bind:this` in `Root.svelte`.
160
+ * @type {import('svelte').SvelteComponent[]}
142
161
  */
143
- export function create_client(app, target) {
144
- const routes = parse(app);
162
+ const components = [];
145
163
 
146
- const default_layout_loader = app.nodes[0];
147
- const default_error_loader = app.nodes[1];
164
+ /** @type {{id: string, promise: Promise<import('./types.js').NavigationResult>} | null} */
165
+ let load_cache = null;
148
166
 
149
- // we import the root layout/error nodes eagerly, so that
150
- // connectivity errors after initialisation don't nuke the app
151
- default_layout_loader();
152
- default_error_loader();
167
+ /** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */
168
+ const before_navigate_callbacks = [];
153
169
 
154
- const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
170
+ /** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
171
+ const on_navigate_callbacks = [];
155
172
 
156
- /** @type {Array<((url: URL) => boolean)>} */
157
- const invalidated = [];
173
+ /** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */
174
+ let after_navigate_callbacks = [];
158
175
 
159
- /**
160
- * An array of the `+layout.svelte` and `+page.svelte` component instances
161
- * that currently live on the page — used for capturing and restoring snapshots.
162
- * It's updated/manipulated through `bind:this` in `Root.svelte`.
163
- * @type {import('svelte').SvelteComponent[]}
164
- */
165
- const components = [];
176
+ /** @type {import('./types.js').NavigationState} */
177
+ let current = {
178
+ branch: [],
179
+ error: null,
180
+ // @ts-ignore - we need the initial value to be null
181
+ url: null
182
+ };
166
183
 
167
- /** @type {{id: string, promise: Promise<import('./types.js').NavigationResult>} | null} */
168
- let load_cache = null;
184
+ /** this being true means we SSR'd */
185
+ let hydrated = false;
186
+ let started = false;
187
+ let autoscroll = true;
188
+ let updating = false;
189
+ let navigating = false;
190
+ let hash_navigating = false;
191
+ /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
192
+ let has_navigated = false;
169
193
 
170
- const callbacks = {
171
- /** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */
172
- before_navigate: [],
194
+ let force_invalidation = false;
173
195
 
174
- /** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
175
- on_navigate: [],
196
+ /** @type {import('svelte').SvelteComponent} */
197
+ let root;
176
198
 
177
- /** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */
178
- after_navigate: []
179
- };
199
+ /** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
200
+ let current_history_index;
180
201
 
181
- /** @type {import('./types.js').NavigationState} */
182
- let current = {
183
- branch: [],
184
- error: null,
185
- // @ts-ignore - we need the initial value to be null
186
- url: null
187
- };
202
+ /** @type {number} */
203
+ let current_navigation_index;
204
+
205
+ /** @type {import('@sveltejs/kit').Page} */
206
+ let page;
207
+
208
+ /** @type {{}} */
209
+ let token;
188
210
 
189
- /** this being true means we SSR'd */
190
- let hydrated = false;
191
- let started = false;
192
- let autoscroll = true;
193
- let updating = false;
194
- let navigating = false;
195
- let hash_navigating = false;
196
- /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
197
- let has_navigated = false;
211
+ /** @type {Promise<void> | null} */
212
+ let pending_invalidate;
198
213
 
199
- let force_invalidation = false;
214
+ /**
215
+ * @param {import('./types.js').SvelteKitApp} _app
216
+ * @param {HTMLElement} _target
217
+ * @param {Parameters<typeof _hydrate>[1]} [hydrate]
218
+ */
219
+ export async function start(_app, _target, hydrate) {
220
+ if (DEV && _target === document.body) {
221
+ console.warn(
222
+ 'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>'
223
+ );
224
+ }
200
225
 
201
- /** @type {import('svelte').SvelteComponent} */
202
- let root;
226
+ app = _app;
227
+ routes = parse(_app);
228
+ container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
229
+ target = _target;
203
230
 
204
- // keeping track of the history index in order to prevent popstate navigation events if needed
205
- /** @type {number} */
206
- let current_history_index = history.state?.[HISTORY_INDEX];
231
+ // we import the root layout/error nodes eagerly, so that
232
+ // connectivity errors after initialisation don't nuke the app
233
+ default_layout_loader = _app.nodes[0];
234
+ default_error_loader = _app.nodes[1];
235
+ default_layout_loader();
236
+ default_error_loader();
207
237
 
208
- /** @type {number} */
209
- let current_navigation_index = history.state?.[NAVIGATION_INDEX];
238
+ current_history_index = history.state?.[HISTORY_INDEX];
239
+ current_navigation_index = history.state?.[NAVIGATION_INDEX];
210
240
 
211
241
  if (!current_history_index) {
212
242
  // we use Date.now() as an offset so that cross-document navigations
@@ -234,1876 +264,2026 @@ export function create_client(app, target) {
234
264
  scrollTo(scroll.x, scroll.y);
235
265
  }
236
266
 
237
- /** @type {import('@sveltejs/kit').Page} */
238
- let page;
239
-
240
- /** @type {{}} */
241
- let token;
267
+ if (hydrate) {
268
+ await _hydrate(target, hydrate);
269
+ } else {
270
+ goto(location.href, { replaceState: true });
271
+ }
242
272
 
243
- /** @type {Promise<void> | null} */
244
- let pending_invalidate;
273
+ _start_router();
274
+ }
245
275
 
246
- async function invalidate() {
247
- // Accept all invalidations as they come, don't swallow any while another invalidation
248
- // is running because subsequent invalidations may make earlier ones outdated,
249
- // but batch multiple synchronous invalidations.
250
- await (pending_invalidate ||= Promise.resolve());
251
- if (!pending_invalidate) return;
252
- pending_invalidate = null;
276
+ async function _invalidate() {
277
+ // Accept all invalidations as they come, don't swallow any while another invalidation
278
+ // is running because subsequent invalidations may make earlier ones outdated,
279
+ // but batch multiple synchronous invalidations.
280
+ await (pending_invalidate ||= Promise.resolve());
281
+ if (!pending_invalidate) return;
282
+ pending_invalidate = null;
253
283
 
254
- const intent = get_navigation_intent(current.url, true);
284
+ const intent = get_navigation_intent(current.url, true);
255
285
 
256
- // Clear preload, it might be affected by the invalidation.
257
- // Also solves an edge case where a preload is triggered, the navigation for it
258
- // was then triggered and is still running while the invalidation kicks in,
259
- // at which point the invalidation should take over and "win".
260
- load_cache = null;
286
+ // Clear preload, it might be affected by the invalidation.
287
+ // Also solves an edge case where a preload is triggered, the navigation for it
288
+ // was then triggered and is still running while the invalidation kicks in,
289
+ // at which point the invalidation should take over and "win".
290
+ load_cache = null;
261
291
 
262
- const nav_token = (token = {});
263
- const navigation_result = intent && (await load_route(intent));
264
- if (nav_token !== token) return;
292
+ const nav_token = (token = {});
293
+ const navigation_result = intent && (await load_route(intent));
294
+ if (nav_token !== token) return;
265
295
 
266
- if (navigation_result) {
267
- if (navigation_result.type === 'redirect') {
268
- await goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token);
269
- } else {
270
- if (navigation_result.props.page !== undefined) {
271
- page = navigation_result.props.page;
272
- }
273
- root.$set(navigation_result.props);
296
+ if (navigation_result) {
297
+ if (navigation_result.type === 'redirect') {
298
+ await _goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token);
299
+ } else {
300
+ if (navigation_result.props.page !== undefined) {
301
+ page = navigation_result.props.page;
274
302
  }
303
+ root.$set(navigation_result.props);
275
304
  }
276
-
277
- invalidated.length = 0;
278
305
  }
279
306
 
280
- /** @param {number} index */
281
- function capture_snapshot(index) {
282
- if (components.some((c) => c?.snapshot)) {
283
- snapshots[index] = components.map((c) => c?.snapshot?.capture());
284
- }
285
- }
307
+ invalidated.length = 0;
308
+ }
286
309
 
287
- /** @param {number} index */
288
- function restore_snapshot(index) {
289
- snapshots[index]?.forEach((value, i) => {
290
- components[i]?.snapshot?.restore(value);
291
- });
310
+ /** @param {number} index */
311
+ function capture_snapshot(index) {
312
+ if (components.some((c) => c?.snapshot)) {
313
+ snapshots[index] = components.map((c) => c?.snapshot?.capture());
292
314
  }
315
+ }
316
+
317
+ /** @param {number} index */
318
+ function restore_snapshot(index) {
319
+ snapshots[index]?.forEach((value, i) => {
320
+ components[i]?.snapshot?.restore(value);
321
+ });
322
+ }
293
323
 
294
- function persist_state() {
295
- update_scroll_positions(current_history_index);
296
- storage.set(SCROLL_KEY, scroll_positions);
324
+ function persist_state() {
325
+ update_scroll_positions(current_history_index);
326
+ storage.set(SCROLL_KEY, scroll_positions);
297
327
 
298
- capture_snapshot(current_navigation_index);
299
- storage.set(SNAPSHOT_KEY, snapshots);
300
- storage.set(STATES_KEY, states, devalue.stringify);
301
- }
328
+ capture_snapshot(current_navigation_index);
329
+ storage.set(SNAPSHOT_KEY, snapshots);
330
+ }
302
331
 
303
- /**
304
- * @param {string | URL} url
305
- * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; state?: Record<string, any> }} options
306
- * @param {number} redirect_count
307
- * @param {{}} [nav_token]
308
- */
309
- async function goto(url, options, redirect_count, nav_token) {
310
- return navigate({
311
- type: 'goto',
312
- url: resolve_url(url),
313
- keepfocus: options.keepFocus,
314
- noscroll: options.noScroll,
315
- replace_state: options.replaceState,
316
- redirect_count,
317
- state: options.state,
318
- nav_token,
319
- accept: () => {
320
- if (options.invalidateAll) {
321
- force_invalidation = true;
322
- }
332
+ /**
333
+ * @param {string | URL} url
334
+ * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; state?: Record<string, any> }} options
335
+ * @param {number} redirect_count
336
+ * @param {{}} [nav_token]
337
+ */
338
+ async function _goto(url, options, redirect_count, nav_token) {
339
+ return navigate({
340
+ type: 'goto',
341
+ url: resolve_url(url),
342
+ keepfocus: options.keepFocus,
343
+ noscroll: options.noScroll,
344
+ replace_state: options.replaceState,
345
+ state: options.state,
346
+ redirect_count,
347
+ nav_token,
348
+ accept: () => {
349
+ if (options.invalidateAll) {
350
+ force_invalidation = true;
323
351
  }
324
- });
325
- }
352
+ }
353
+ });
354
+ }
326
355
 
327
- /** @param {import('./types.js').NavigationIntent} intent */
328
- async function preload_data(intent) {
329
- load_cache = {
330
- id: intent.id,
331
- promise: load_route(intent).then((result) => {
332
- if (result.type === 'loaded' && result.state.error) {
333
- // Don't cache errors, because they might be transient
334
- load_cache = null;
335
- }
336
- return result;
337
- })
338
- };
356
+ /** @param {import('./types.js').NavigationIntent} intent */
357
+ async function _preload_data(intent) {
358
+ load_cache = {
359
+ id: intent.id,
360
+ promise: load_route(intent).then((result) => {
361
+ if (result.type === 'loaded' && result.state.error) {
362
+ // Don't cache errors, because they might be transient
363
+ load_cache = null;
364
+ }
365
+ return result;
366
+ })
367
+ };
339
368
 
340
- return load_cache.promise;
341
- }
369
+ return load_cache.promise;
370
+ }
342
371
 
343
- /** @param {string} pathname */
344
- async function preload_code(pathname) {
345
- const route = routes.find((route) => route.exec(get_url_path(pathname)));
372
+ /** @param {string} pathname */
373
+ async function _preload_code(pathname) {
374
+ const route = routes.find((route) => route.exec(get_url_path(pathname)));
346
375
 
347
- if (route) {
348
- await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]()));
349
- }
376
+ if (route) {
377
+ await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]()));
350
378
  }
379
+ }
351
380
 
352
- /** @param {import('./types.js').NavigationFinished} result */
353
- function initialize(result) {
354
- if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return;
381
+ /**
382
+ * @param {import('./types.js').NavigationFinished} result
383
+ * @param {HTMLElement} target
384
+ */
385
+ function initialize(result, target) {
386
+ if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return;
355
387
 
356
- current = result.state;
388
+ current = result.state;
357
389
 
358
- const style = document.querySelector('style[data-sveltekit]');
359
- if (style) style.remove();
390
+ const style = document.querySelector('style[data-sveltekit]');
391
+ if (style) style.remove();
360
392
 
361
- page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page);
393
+ page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page);
362
394
 
363
- root = new app.root({
364
- target,
365
- props: { ...result.props, stores, components },
366
- hydrate: true
367
- });
395
+ root = new app.root({
396
+ target,
397
+ props: { ...result.props, stores, components },
398
+ hydrate: true
399
+ });
368
400
 
369
- restore_snapshot(current_navigation_index);
401
+ restore_snapshot(current_navigation_index);
370
402
 
371
- /** @type {import('@sveltejs/kit').AfterNavigate} */
372
- const navigation = {
373
- from: null,
374
- to: {
375
- params: current.params,
376
- route: { id: current.route?.id ?? null },
377
- url: new URL(location.href)
378
- },
379
- willUnload: false,
380
- type: 'enter',
381
- complete: Promise.resolve()
382
- };
383
- callbacks.after_navigate.forEach((fn) => fn(navigation));
403
+ /** @type {import('@sveltejs/kit').AfterNavigate} */
404
+ const navigation = {
405
+ from: null,
406
+ to: {
407
+ params: current.params,
408
+ route: { id: current.route?.id ?? null },
409
+ url: new URL(location.href)
410
+ },
411
+ willUnload: false,
412
+ type: 'enter',
413
+ complete: Promise.resolve()
414
+ };
384
415
 
385
- started = true;
386
- }
416
+ after_navigate_callbacks.forEach((fn) => fn(navigation));
387
417
 
388
- /**
389
- *
390
- * @param {{
391
- * url: URL;
392
- * params: Record<string, string>;
393
- * branch: Array<import('./types.js').BranchNode | undefined>;
394
- * status: number;
395
- * error: App.Error | null;
396
- * route: import('types').CSRRoute | null;
397
- * form?: Record<string, any> | null;
398
- * }} opts
399
- */
400
- async function get_navigation_result_from_branch({
401
- url,
402
- params,
403
- branch,
404
- status,
405
- error,
406
- route,
407
- form
408
- }) {
409
- /** @type {import('types').TrailingSlash} */
410
- let slash = 'never';
411
- for (const node of branch) {
412
- if (node?.slash !== undefined) slash = node.slash;
413
- }
414
- url.pathname = normalize_path(url.pathname, slash);
415
- // eslint-disable-next-line
416
- url.search = url.search; // turn `/?` into `/`
417
-
418
- /** @type {import('./types.js').NavigationFinished} */
419
- const result = {
420
- type: 'loaded',
421
- state: {
422
- url,
423
- params,
424
- branch,
425
- error,
426
- route
427
- },
428
- props: {
429
- // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
430
- constructors: compact(branch).map((branch_node) => branch_node.node.component),
431
- page
432
- }
433
- };
418
+ started = true;
419
+ }
420
+
421
+ /**
422
+ *
423
+ * @param {{
424
+ * url: URL;
425
+ * params: Record<string, string>;
426
+ * branch: Array<import('./types.js').BranchNode | undefined>;
427
+ * status: number;
428
+ * error: App.Error | null;
429
+ * route: import('types').CSRRoute | null;
430
+ * form?: Record<string, any> | null;
431
+ * }} opts
432
+ */
433
+ async function get_navigation_result_from_branch({
434
+ url,
435
+ params,
436
+ branch,
437
+ status,
438
+ error,
439
+ route,
440
+ form
441
+ }) {
442
+ /** @type {import('types').TrailingSlash} */
443
+ let slash = 'never';
444
+ for (const node of branch) {
445
+ if (node?.slash !== undefined) slash = node.slash;
446
+ }
447
+ url.pathname = normalize_path(url.pathname, slash);
448
+ // eslint-disable-next-line
449
+ url.search = url.search; // turn `/?` into `/`
434
450
 
435
- if (form !== undefined) {
436
- result.props.form = form;
451
+ /** @type {import('./types.js').NavigationFinished} */
452
+ const result = {
453
+ type: 'loaded',
454
+ state: {
455
+ url,
456
+ params,
457
+ branch,
458
+ error,
459
+ route
460
+ },
461
+ props: {
462
+ // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up
463
+ constructors: compact(branch).map((branch_node) => branch_node.node.component),
464
+ page
437
465
  }
466
+ };
438
467
 
439
- let data = {};
440
- let data_changed = !page;
468
+ if (form !== undefined) {
469
+ result.props.form = form;
470
+ }
441
471
 
442
- let p = 0;
472
+ let data = {};
473
+ let data_changed = !page;
443
474
 
444
- for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) {
445
- const node = branch[i];
446
- const prev = current.branch[i];
475
+ let p = 0;
447
476
 
448
- if (node?.data !== prev?.data) data_changed = true;
449
- if (!node) continue;
477
+ for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) {
478
+ const node = branch[i];
479
+ const prev = current.branch[i];
450
480
 
451
- data = { ...data, ...node.data };
481
+ if (node?.data !== prev?.data) data_changed = true;
482
+ if (!node) continue;
452
483
 
453
- // Only set props if the node actually updated. This prevents needless rerenders.
454
- if (data_changed) {
455
- result.props[`data_${p}`] = data;
456
- }
484
+ data = { ...data, ...node.data };
457
485
 
458
- p += 1;
486
+ // Only set props if the node actually updated. This prevents needless rerenders.
487
+ if (data_changed) {
488
+ result.props[`data_${p}`] = data;
459
489
  }
460
490
 
461
- const page_changed =
462
- !current.url ||
463
- url.href !== current.url.href ||
464
- current.error !== error ||
465
- (form !== undefined && form !== page.form) ||
466
- data_changed;
491
+ p += 1;
492
+ }
467
493
 
468
- if (page_changed) {
469
- result.props.page = {
470
- error,
471
- params,
472
- route: {
473
- id: route?.id ?? null
474
- },
475
- state: {},
476
- status,
477
- url: new URL(url),
478
- form: form ?? null,
479
- // The whole page store is updated, but this way the object reference stays the same
480
- data: data_changed ? data : page.data
481
- };
482
- }
494
+ const page_changed =
495
+ !current.url ||
496
+ url.href !== current.url.href ||
497
+ current.error !== error ||
498
+ (form !== undefined && form !== page.form) ||
499
+ data_changed;
483
500
 
484
- return result;
501
+ if (page_changed) {
502
+ result.props.page = {
503
+ error,
504
+ params,
505
+ route: {
506
+ id: route?.id ?? null
507
+ },
508
+ state: {},
509
+ status,
510
+ url: new URL(url),
511
+ form: form ?? null,
512
+ // The whole page store is updated, but this way the object reference stays the same
513
+ data: data_changed ? data : page.data
514
+ };
485
515
  }
486
516
 
487
- /**
488
- * Call the load function of the given node, if it exists.
489
- * If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested.
490
- *
491
- * @param {{
492
- * loader: import('types').CSRPageNodeLoader;
493
- * parent: () => Promise<Record<string, any>>;
494
- * url: URL;
495
- * params: Record<string, string>;
496
- * route: { id: string | null };
497
- * server_data_node: import('./types.js').DataNode | null;
498
- * }} options
499
- * @returns {Promise<import('./types.js').BranchNode>}
500
- */
501
- async function load_node({ loader, parent, url, params, route, server_data_node }) {
502
- /** @type {Record<string, any> | null} */
503
- let data = null;
504
-
505
- let is_tracking = true;
506
-
507
- /** @type {import('types').Uses} */
508
- const uses = {
509
- dependencies: new Set(),
510
- params: new Set(),
511
- parent: false,
512
- route: false,
513
- url: false,
514
- search_params: new Set()
515
- };
517
+ return result;
518
+ }
516
519
 
517
- const node = await loader();
520
+ /**
521
+ * Call the load function of the given node, if it exists.
522
+ * If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested.
523
+ *
524
+ * @param {{
525
+ * loader: import('types').CSRPageNodeLoader;
526
+ * parent: () => Promise<Record<string, any>>;
527
+ * url: URL;
528
+ * params: Record<string, string>;
529
+ * route: { id: string | null };
530
+ * server_data_node: import('./types.js').DataNode | null;
531
+ * }} options
532
+ * @returns {Promise<import('./types.js').BranchNode>}
533
+ */
534
+ async function load_node({ loader, parent, url, params, route, server_data_node }) {
535
+ /** @type {Record<string, any> | null} */
536
+ let data = null;
537
+
538
+ let is_tracking = true;
539
+
540
+ /** @type {import('types').Uses} */
541
+ const uses = {
542
+ dependencies: new Set(),
543
+ params: new Set(),
544
+ parent: false,
545
+ route: false,
546
+ url: false,
547
+ search_params: new Set()
548
+ };
518
549
 
519
- if (DEV) {
520
- validate_page_exports(node.universal);
521
- }
550
+ const node = await loader();
522
551
 
523
- if (node.universal?.load) {
524
- /** @param {string[]} deps */
525
- function depends(...deps) {
526
- for (const dep of deps) {
527
- if (DEV) validate_depends(/** @type {string} */ (route.id), dep);
552
+ if (DEV) {
553
+ validate_page_exports(node.universal);
554
+ }
528
555
 
529
- const { href } = new URL(dep, url);
530
- uses.dependencies.add(href);
531
- }
532
- }
556
+ if (node.universal?.load) {
557
+ /** @param {string[]} deps */
558
+ function depends(...deps) {
559
+ for (const dep of deps) {
560
+ if (DEV) validate_depends(/** @type {string} */ (route.id), dep);
533
561
 
534
- /** @type {import('@sveltejs/kit').LoadEvent} */
535
- const load_input = {
536
- route: new Proxy(route, {
537
- get: (target, key) => {
538
- if (is_tracking) {
539
- uses.route = true;
540
- }
541
- return target[/** @type {'id'} */ (key)];
542
- }
543
- }),
544
- params: new Proxy(params, {
545
- get: (target, key) => {
546
- if (is_tracking) {
547
- uses.params.add(/** @type {string} */ (key));
548
- }
549
- return target[/** @type {string} */ (key)];
550
- }
551
- }),
552
- data: server_data_node?.data ?? null,
553
- url: make_trackable(
554
- url,
555
- () => {
556
- if (is_tracking) {
557
- uses.url = true;
558
- }
559
- },
560
- (param) => {
561
- if (is_tracking) {
562
- uses.search_params.add(param);
563
- }
564
- }
565
- ),
566
- async fetch(resource, init) {
567
- /** @type {URL | string} */
568
- let requested;
569
-
570
- if (resource instanceof Request) {
571
- requested = resource.url;
572
-
573
- // we're not allowed to modify the received `Request` object, so in order
574
- // to fixup relative urls we create a new equivalent `init` object instead
575
- init = {
576
- // the request body must be consumed in memory until browsers
577
- // implement streaming request bodies and/or the body getter
578
- body:
579
- resource.method === 'GET' || resource.method === 'HEAD'
580
- ? undefined
581
- : await resource.blob(),
582
- cache: resource.cache,
583
- credentials: resource.credentials,
584
- headers: resource.headers,
585
- integrity: resource.integrity,
586
- keepalive: resource.keepalive,
587
- method: resource.method,
588
- mode: resource.mode,
589
- redirect: resource.redirect,
590
- referrer: resource.referrer,
591
- referrerPolicy: resource.referrerPolicy,
592
- signal: resource.signal,
593
- ...init
594
- };
595
- } else {
596
- requested = resource;
597
- }
562
+ const { href } = new URL(dep, url);
563
+ uses.dependencies.add(href);
564
+ }
565
+ }
598
566
 
599
- // we must fixup relative urls so they are resolved from the target page
600
- const resolved = new URL(requested, url);
567
+ /** @type {import('@sveltejs/kit').LoadEvent} */
568
+ const load_input = {
569
+ route: new Proxy(route, {
570
+ get: (target, key) => {
601
571
  if (is_tracking) {
602
- depends(resolved.href);
572
+ uses.route = true;
603
573
  }
604
-
605
- // match ssr serialized data url, which is important to find cached responses
606
- if (resolved.origin === url.origin) {
607
- requested = resolved.href.slice(url.origin.length);
574
+ return target[/** @type {'id'} */ (key)];
575
+ }
576
+ }),
577
+ params: new Proxy(params, {
578
+ get: (target, key) => {
579
+ if (is_tracking) {
580
+ uses.params.add(/** @type {string} */ (key));
608
581
  }
609
-
610
- // prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
611
- return started
612
- ? subsequent_fetch(requested, resolved.href, init)
613
- : initial_fetch(requested, init);
614
- },
615
- setHeaders: () => {}, // noop
616
- depends,
617
- parent() {
582
+ return target[/** @type {string} */ (key)];
583
+ }
584
+ }),
585
+ data: server_data_node?.data ?? null,
586
+ url: make_trackable(
587
+ url,
588
+ () => {
618
589
  if (is_tracking) {
619
- uses.parent = true;
590
+ uses.url = true;
620
591
  }
621
- return parent();
622
592
  },
623
- untrack(fn) {
624
- is_tracking = false;
625
- try {
626
- return fn();
627
- } finally {
628
- is_tracking = true;
593
+ (param) => {
594
+ if (is_tracking) {
595
+ uses.search_params.add(param);
629
596
  }
630
597
  }
631
- };
598
+ ),
599
+ async fetch(resource, init) {
600
+ /** @type {URL | string} */
601
+ let requested;
602
+
603
+ if (resource instanceof Request) {
604
+ requested = resource.url;
605
+
606
+ // we're not allowed to modify the received `Request` object, so in order
607
+ // to fixup relative urls we create a new equivalent `init` object instead
608
+ init = {
609
+ // the request body must be consumed in memory until browsers
610
+ // implement streaming request bodies and/or the body getter
611
+ body:
612
+ resource.method === 'GET' || resource.method === 'HEAD'
613
+ ? undefined
614
+ : await resource.blob(),
615
+ cache: resource.cache,
616
+ credentials: resource.credentials,
617
+ headers: resource.headers,
618
+ integrity: resource.integrity,
619
+ keepalive: resource.keepalive,
620
+ method: resource.method,
621
+ mode: resource.mode,
622
+ redirect: resource.redirect,
623
+ referrer: resource.referrer,
624
+ referrerPolicy: resource.referrerPolicy,
625
+ signal: resource.signal,
626
+ ...init
627
+ };
628
+ } else {
629
+ requested = resource;
630
+ }
631
+
632
+ // we must fixup relative urls so they are resolved from the target page
633
+ const resolved = new URL(requested, url);
634
+ if (is_tracking) {
635
+ depends(resolved.href);
636
+ }
632
637
 
633
- if (DEV) {
638
+ // match ssr serialized data url, which is important to find cached responses
639
+ if (resolved.origin === url.origin) {
640
+ requested = resolved.href.slice(url.origin.length);
641
+ }
642
+
643
+ // prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
644
+ return started
645
+ ? subsequent_fetch(requested, resolved.href, init)
646
+ : initial_fetch(requested, init);
647
+ },
648
+ setHeaders: () => {}, // noop
649
+ depends,
650
+ parent() {
651
+ if (is_tracking) {
652
+ uses.parent = true;
653
+ }
654
+ return parent();
655
+ },
656
+ untrack(fn) {
657
+ is_tracking = false;
634
658
  try {
635
- lock_fetch();
636
- data = (await node.universal.load.call(null, load_input)) ?? null;
637
- if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
638
- throw new Error(
639
- `a load function related to route '${route.id}' returned ${
640
- typeof data !== 'object'
641
- ? `a ${typeof data}`
642
- : data instanceof Response
643
- ? 'a Response object'
644
- : Array.isArray(data)
645
- ? 'an array'
646
- : 'a non-plain object'
647
- }, but must return a plain object at the top level (i.e. \`return {...}\`)`
648
- );
649
- }
659
+ return fn();
650
660
  } finally {
651
- unlock_fetch();
661
+ is_tracking = true;
652
662
  }
653
- } else {
654
- data = (await node.universal.load.call(null, load_input)) ?? null;
655
663
  }
656
- }
657
-
658
- return {
659
- node,
660
- loader,
661
- server: server_data_node,
662
- universal: node.universal?.load ? { type: 'data', data, uses } : null,
663
- data: data ?? server_data_node?.data ?? null,
664
- // if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of
665
- // the `trailingSlash` route option, so that relative paths to JS and CSS work
666
- slash:
667
- base && (url.pathname === base || url.pathname === base + '/')
668
- ? 'always'
669
- : node.universal?.trailingSlash ?? server_data_node?.slash
670
664
  };
671
- }
672
665
 
673
- /**
674
- * @param {boolean} parent_changed
675
- * @param {boolean} route_changed
676
- * @param {boolean} url_changed
677
- * @param {Set<string>} search_params_changed
678
- * @param {import('types').Uses | undefined} uses
679
- * @param {Record<string, string>} params
680
- */
681
- function has_changed(
682
- parent_changed,
683
- route_changed,
684
- url_changed,
685
- search_params_changed,
686
- uses,
687
- params
688
- ) {
689
- if (force_invalidation) return true;
690
-
691
- if (!uses) return false;
692
-
693
- if (uses.parent && parent_changed) return true;
694
- if (uses.route && route_changed) return true;
695
- if (uses.url && url_changed) return true;
696
-
697
- for (const tracked_params of uses.search_params) {
698
- if (search_params_changed.has(tracked_params)) return true;
666
+ if (DEV) {
667
+ try {
668
+ lock_fetch();
669
+ data = (await node.universal.load.call(null, load_input)) ?? null;
670
+ if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
671
+ throw new Error(
672
+ `a load function related to route '${route.id}' returned ${
673
+ typeof data !== 'object'
674
+ ? `a ${typeof data}`
675
+ : data instanceof Response
676
+ ? 'a Response object'
677
+ : Array.isArray(data)
678
+ ? 'an array'
679
+ : 'a non-plain object'
680
+ }, but must return a plain object at the top level (i.e. \`return {...}\`)`
681
+ );
682
+ }
683
+ } finally {
684
+ unlock_fetch();
685
+ }
686
+ } else {
687
+ data = (await node.universal.load.call(null, load_input)) ?? null;
699
688
  }
689
+ }
700
690
 
701
- for (const param of uses.params) {
702
- if (params[param] !== current.params[param]) return true;
703
- }
691
+ return {
692
+ node,
693
+ loader,
694
+ server: server_data_node,
695
+ universal: node.universal?.load ? { type: 'data', data, uses } : null,
696
+ data: data ?? server_data_node?.data ?? null,
697
+ // if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of
698
+ // the `trailingSlash` route option, so that relative paths to JS and CSS work
699
+ slash:
700
+ base && (url.pathname === base || url.pathname === base + '/')
701
+ ? 'always'
702
+ : node.universal?.trailingSlash ?? server_data_node?.slash
703
+ };
704
+ }
704
705
 
705
- for (const href of uses.dependencies) {
706
- if (invalidated.some((fn) => fn(new URL(href)))) return true;
707
- }
706
+ /**
707
+ * @param {boolean} parent_changed
708
+ * @param {boolean} route_changed
709
+ * @param {boolean} url_changed
710
+ * @param {Set<string>} search_params_changed
711
+ * @param {import('types').Uses | undefined} uses
712
+ * @param {Record<string, string>} params
713
+ */
714
+ function has_changed(
715
+ parent_changed,
716
+ route_changed,
717
+ url_changed,
718
+ search_params_changed,
719
+ uses,
720
+ params
721
+ ) {
722
+ if (force_invalidation) return true;
723
+
724
+ if (!uses) return false;
725
+
726
+ if (uses.parent && parent_changed) return true;
727
+ if (uses.route && route_changed) return true;
728
+ if (uses.url && url_changed) return true;
729
+
730
+ for (const tracked_params of uses.search_params) {
731
+ if (search_params_changed.has(tracked_params)) return true;
732
+ }
708
733
 
709
- return false;
734
+ for (const param of uses.params) {
735
+ if (params[param] !== current.params[param]) return true;
710
736
  }
711
737
 
712
- /**
713
- * @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node
714
- * @param {import('./types.js').DataNode | null} [previous]
715
- * @returns {import('./types.js').DataNode | null}
716
- */
717
- function create_data_node(node, previous) {
718
- if (node?.type === 'data') return node;
719
- if (node?.type === 'skip') return previous ?? null;
720
- return null;
738
+ for (const href of uses.dependencies) {
739
+ if (invalidated.some((fn) => fn(new URL(href)))) return true;
721
740
  }
722
741
 
723
- /**
724
- *
725
- * @param {URL | null} old_url
726
- * @param {URL} new_url
727
- */
728
- function diff_search_params(old_url, new_url) {
729
- if (!old_url) return new Set(new_url.searchParams.keys());
742
+ return false;
743
+ }
730
744
 
731
- const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]);
745
+ /**
746
+ * @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node
747
+ * @param {import('./types.js').DataNode | null} [previous]
748
+ * @returns {import('./types.js').DataNode | null}
749
+ */
750
+ function create_data_node(node, previous) {
751
+ if (node?.type === 'data') return node;
752
+ if (node?.type === 'skip') return previous ?? null;
753
+ return null;
754
+ }
732
755
 
733
- for (const key of changed) {
734
- const old_values = old_url.searchParams.getAll(key);
735
- const new_values = new_url.searchParams.getAll(key);
756
+ /**
757
+ *
758
+ * @param {URL | null} old_url
759
+ * @param {URL} new_url
760
+ */
761
+ function diff_search_params(old_url, new_url) {
762
+ if (!old_url) return new Set(new_url.searchParams.keys());
736
763
 
737
- if (
738
- old_values.every((value) => new_values.includes(value)) &&
739
- new_values.every((value) => old_values.includes(value))
740
- ) {
741
- changed.delete(key);
742
- }
743
- }
764
+ const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]);
744
765
 
745
- return changed;
746
- }
766
+ for (const key of changed) {
767
+ const old_values = old_url.searchParams.getAll(key);
768
+ const new_values = new_url.searchParams.getAll(key);
747
769
 
748
- /**
749
- * @param {import('./types.js').NavigationIntent} intent
750
- * @returns {Promise<import('./types.js').NavigationResult>}
751
- */
752
- async function load_route({ id, invalidating, url, params, route }) {
753
- if (load_cache?.id === id) {
754
- return load_cache.promise;
770
+ if (
771
+ old_values.every((value) => new_values.includes(value)) &&
772
+ new_values.every((value) => old_values.includes(value))
773
+ ) {
774
+ changed.delete(key);
755
775
  }
776
+ }
756
777
 
757
- const { errors, layouts, leaf } = route;
758
-
759
- const loaders = [...layouts, leaf];
760
-
761
- // preload modules to avoid waterfall, but handle rejections
762
- // so they don't get reported to Sentry et al (we don't need
763
- // to act on the failures at this point)
764
- errors.forEach((loader) => loader?.().catch(() => {}));
765
- loaders.forEach((loader) => loader?.[1]().catch(() => {}));
766
-
767
- /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
768
- let server_data = null;
769
- const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
770
- const route_changed = current.route ? route.id !== current.route.id : false;
771
- const search_params_changed = diff_search_params(current.url, url);
772
-
773
- let parent_invalid = false;
774
- const invalid_server_nodes = loaders.map((loader, i) => {
775
- const previous = current.branch[i];
776
-
777
- const invalid =
778
- !!loader?.[0] &&
779
- (previous?.loader !== loader[1] ||
780
- has_changed(
781
- parent_invalid,
782
- route_changed,
783
- url_changed,
784
- search_params_changed,
785
- previous.server?.uses,
786
- params
787
- ));
788
-
789
- if (invalid) {
790
- // For the next one
791
- parent_invalid = true;
792
- }
793
-
794
- return invalid;
795
- });
796
-
797
- if (invalid_server_nodes.some(Boolean)) {
798
- try {
799
- server_data = await load_data(url, invalid_server_nodes);
800
- } catch (error) {
801
- return load_root_error_page({
802
- status: get_status(error),
803
- error: await handle_error(error, { url, params, route: { id: route.id } }),
804
- url,
805
- route
806
- });
807
- }
778
+ return changed;
779
+ }
808
780
 
809
- if (server_data.type === 'redirect') {
810
- return server_data;
811
- }
812
- }
781
+ /**
782
+ * @param {import('./types.js').NavigationIntent} intent
783
+ * @returns {Promise<import('./types.js').NavigationResult>}
784
+ */
785
+ async function load_route({ id, invalidating, url, params, route }) {
786
+ if (load_cache?.id === id) {
787
+ return load_cache.promise;
788
+ }
813
789
 
814
- const server_data_nodes = server_data?.nodes;
790
+ const { errors, layouts, leaf } = route;
815
791
 
816
- let parent_changed = false;
792
+ const loaders = [...layouts, leaf];
817
793
 
818
- const branch_promises = loaders.map(async (loader, i) => {
819
- if (!loader) return;
794
+ // preload modules to avoid waterfall, but handle rejections
795
+ // so they don't get reported to Sentry et al (we don't need
796
+ // to act on the failures at this point)
797
+ errors.forEach((loader) => loader?.().catch(() => {}));
798
+ loaders.forEach((loader) => loader?.[1]().catch(() => {}));
820
799
 
821
- /** @type {import('./types.js').BranchNode | undefined} */
822
- const previous = current.branch[i];
800
+ /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
801
+ let server_data = null;
802
+ const url_changed = current.url ? id !== current.url.pathname + current.url.search : false;
803
+ const route_changed = current.route ? route.id !== current.route.id : false;
804
+ const search_params_changed = diff_search_params(current.url, url);
823
805
 
824
- const server_data_node = server_data_nodes?.[i];
806
+ let parent_invalid = false;
807
+ const invalid_server_nodes = loaders.map((loader, i) => {
808
+ const previous = current.branch[i];
825
809
 
826
- // re-use data from previous load if it's still valid
827
- const valid =
828
- (!server_data_node || server_data_node.type === 'skip') &&
829
- loader[1] === previous?.loader &&
830
- !has_changed(
831
- parent_changed,
810
+ const invalid =
811
+ !!loader?.[0] &&
812
+ (previous?.loader !== loader[1] ||
813
+ has_changed(
814
+ parent_invalid,
832
815
  route_changed,
833
816
  url_changed,
834
817
  search_params_changed,
835
- previous.universal?.uses,
818
+ previous.server?.uses,
836
819
  params
837
- );
838
- if (valid) return previous;
820
+ ));
839
821
 
840
- parent_changed = true;
822
+ if (invalid) {
823
+ // For the next one
824
+ parent_invalid = true;
825
+ }
841
826
 
842
- if (server_data_node?.type === 'error') {
843
- // rethrow and catch below
844
- throw server_data_node;
845
- }
827
+ return invalid;
828
+ });
846
829
 
847
- return load_node({
848
- loader: loader[1],
830
+ if (invalid_server_nodes.some(Boolean)) {
831
+ try {
832
+ server_data = await load_data(url, invalid_server_nodes);
833
+ } catch (error) {
834
+ return load_root_error_page({
835
+ status: get_status(error),
836
+ error: await handle_error(error, { url, params, route: { id: route.id } }),
849
837
  url,
850
- params,
851
- route,
852
- parent: async () => {
853
- const data = {};
854
- for (let j = 0; j < i; j += 1) {
855
- Object.assign(data, (await branch_promises[j])?.data);
856
- }
857
- return data;
858
- },
859
- server_data_node: create_data_node(
860
- // server_data_node is undefined if it wasn't reloaded from the server;
861
- // and if current loader uses server data, we want to reuse previous data.
862
- server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null,
863
- loader[0] ? previous?.server : undefined
864
- )
838
+ route
865
839
  });
866
- });
840
+ }
867
841
 
868
- // if we don't do this, rejections will be unhandled
869
- for (const p of branch_promises) p.catch(() => {});
842
+ if (server_data.type === 'redirect') {
843
+ return server_data;
844
+ }
845
+ }
870
846
 
871
- /** @type {Array<import('./types.js').BranchNode | undefined>} */
872
- const branch = [];
847
+ const server_data_nodes = server_data?.nodes;
873
848
 
874
- for (let i = 0; i < loaders.length; i += 1) {
875
- if (loaders[i]) {
876
- try {
877
- branch.push(await branch_promises[i]);
878
- } catch (err) {
879
- if (err instanceof Redirect) {
880
- return {
881
- type: 'redirect',
882
- location: err.location
883
- };
884
- }
849
+ let parent_changed = false;
885
850
 
886
- let status = get_status(err);
887
- /** @type {App.Error} */
888
- let error;
889
-
890
- if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
891
- // this is the server error rethrown above, reconstruct but don't invoke
892
- // the client error handler; it should've already been handled on the server
893
- status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
894
- error = /** @type {import('types').ServerErrorNode} */ (err).error;
895
- } else if (err instanceof HttpError) {
896
- error = err.body;
897
- } else {
898
- // Referenced node could have been removed due to redeploy, check
899
- const updated = await stores.updated.check();
900
- if (updated) {
901
- return await native_navigation(url);
902
- }
851
+ const branch_promises = loaders.map(async (loader, i) => {
852
+ if (!loader) return;
903
853
 
904
- error = await handle_error(err, { params, url, route: { id: route.id } });
905
- }
854
+ /** @type {import('./types.js').BranchNode | undefined} */
855
+ const previous = current.branch[i];
906
856
 
907
- const error_load = await load_nearest_error_page(i, branch, errors);
908
- if (error_load) {
909
- return await get_navigation_result_from_branch({
910
- url,
911
- params,
912
- branch: branch.slice(0, error_load.idx).concat(error_load.node),
913
- status,
914
- error,
915
- route
916
- });
917
- } else {
918
- // if we get here, it's because the root `load` function failed,
919
- // and we need to fall back to the server
920
- return await server_fallback(url, { id: route.id }, error, status);
921
- }
922
- }
923
- } else {
924
- // push an empty slot so we can rewind past gaps to the
925
- // layout that corresponds with an +error.svelte page
926
- branch.push(undefined);
927
- }
857
+ const server_data_node = server_data_nodes?.[i];
858
+
859
+ // re-use data from previous load if it's still valid
860
+ const valid =
861
+ (!server_data_node || server_data_node.type === 'skip') &&
862
+ loader[1] === previous?.loader &&
863
+ !has_changed(
864
+ parent_changed,
865
+ route_changed,
866
+ url_changed,
867
+ search_params_changed,
868
+ previous.universal?.uses,
869
+ params
870
+ );
871
+ if (valid) return previous;
872
+
873
+ parent_changed = true;
874
+
875
+ if (server_data_node?.type === 'error') {
876
+ // rethrow and catch below
877
+ throw server_data_node;
928
878
  }
929
879
 
930
- return await get_navigation_result_from_branch({
880
+ return load_node({
881
+ loader: loader[1],
931
882
  url,
932
883
  params,
933
- branch,
934
- status: 200,
935
- error: null,
936
884
  route,
937
- // Reset `form` on navigation, but not invalidation
938
- form: invalidating ? undefined : null
885
+ parent: async () => {
886
+ const data = {};
887
+ for (let j = 0; j < i; j += 1) {
888
+ Object.assign(data, (await branch_promises[j])?.data);
889
+ }
890
+ return data;
891
+ },
892
+ server_data_node: create_data_node(
893
+ // server_data_node is undefined if it wasn't reloaded from the server;
894
+ // and if current loader uses server data, we want to reuse previous data.
895
+ server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null,
896
+ loader[0] ? previous?.server : undefined
897
+ )
939
898
  });
940
- }
899
+ });
941
900
 
942
- /**
943
- * @param {number} i Start index to backtrack from
944
- * @param {Array<import('./types.js').BranchNode | undefined>} branch Branch to backtrack
945
- * @param {Array<import('types').CSRPageNodeLoader | undefined>} errors All error pages for this branch
946
- * @returns {Promise<{idx: number; node: import('./types.js').BranchNode} | undefined>}
947
- */
948
- async function load_nearest_error_page(i, branch, errors) {
949
- while (i--) {
950
- if (errors[i]) {
951
- let j = i;
952
- while (!branch[j]) j -= 1;
953
- try {
901
+ // if we don't do this, rejections will be unhandled
902
+ for (const p of branch_promises) p.catch(() => {});
903
+
904
+ /** @type {Array<import('./types.js').BranchNode | undefined>} */
905
+ const branch = [];
906
+
907
+ for (let i = 0; i < loaders.length; i += 1) {
908
+ if (loaders[i]) {
909
+ try {
910
+ branch.push(await branch_promises[i]);
911
+ } catch (err) {
912
+ if (err instanceof Redirect) {
954
913
  return {
955
- idx: j + 1,
956
- node: {
957
- node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(),
958
- loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]),
959
- data: {},
960
- server: null,
961
- universal: null
962
- }
914
+ type: 'redirect',
915
+ location: err.location
963
916
  };
964
- } catch (e) {
965
- continue;
917
+ }
918
+
919
+ let status = get_status(err);
920
+ /** @type {App.Error} */
921
+ let error;
922
+
923
+ if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
924
+ // this is the server error rethrown above, reconstruct but don't invoke
925
+ // the client error handler; it should've already been handled on the server
926
+ status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status;
927
+ error = /** @type {import('types').ServerErrorNode} */ (err).error;
928
+ } else if (err instanceof HttpError) {
929
+ error = err.body;
930
+ } else {
931
+ // Referenced node could have been removed due to redeploy, check
932
+ const updated = await stores.updated.check();
933
+ if (updated) {
934
+ return await native_navigation(url);
935
+ }
936
+
937
+ error = await handle_error(err, { params, url, route: { id: route.id } });
938
+ }
939
+
940
+ const error_load = await load_nearest_error_page(i, branch, errors);
941
+ if (error_load) {
942
+ return await get_navigation_result_from_branch({
943
+ url,
944
+ params,
945
+ branch: branch.slice(0, error_load.idx).concat(error_load.node),
946
+ status,
947
+ error,
948
+ route
949
+ });
950
+ } else {
951
+ // if we get here, it's because the root `load` function failed,
952
+ // and we need to fall back to the server
953
+ return await server_fallback(url, { id: route.id }, error, status);
966
954
  }
967
955
  }
956
+ } else {
957
+ // push an empty slot so we can rewind past gaps to the
958
+ // layout that corresponds with an +error.svelte page
959
+ branch.push(undefined);
968
960
  }
969
961
  }
970
962
 
971
- /**
972
- * @param {{
973
- * status: number;
974
- * error: App.Error;
975
- * url: URL;
976
- * route: { id: string | null }
977
- * }} opts
978
- * @returns {Promise<import('./types.js').NavigationFinished>}
979
- */
980
- async function load_root_error_page({ status, error, url, route }) {
981
- /** @type {Record<string, string>} */
982
- const params = {}; // error page does not have params
963
+ return await get_navigation_result_from_branch({
964
+ url,
965
+ params,
966
+ branch,
967
+ status: 200,
968
+ error: null,
969
+ route,
970
+ // Reset `form` on navigation, but not invalidation
971
+ form: invalidating ? undefined : null
972
+ });
973
+ }
974
+
975
+ /**
976
+ * @param {number} i Start index to backtrack from
977
+ * @param {Array<import('./types.js').BranchNode | undefined>} branch Branch to backtrack
978
+ * @param {Array<import('types').CSRPageNodeLoader | undefined>} errors All error pages for this branch
979
+ * @returns {Promise<{idx: number; node: import('./types.js').BranchNode} | undefined>}
980
+ */
981
+ async function load_nearest_error_page(i, branch, errors) {
982
+ while (i--) {
983
+ if (errors[i]) {
984
+ let j = i;
985
+ while (!branch[j]) j -= 1;
986
+ try {
987
+ return {
988
+ idx: j + 1,
989
+ node: {
990
+ node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(),
991
+ loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]),
992
+ data: {},
993
+ server: null,
994
+ universal: null
995
+ }
996
+ };
997
+ } catch (e) {
998
+ continue;
999
+ }
1000
+ }
1001
+ }
1002
+ }
983
1003
 
984
- /** @type {import('types').ServerDataNode | null} */
985
- let server_data_node = null;
1004
+ /**
1005
+ * @param {{
1006
+ * status: number;
1007
+ * error: App.Error;
1008
+ * url: URL;
1009
+ * route: { id: string | null }
1010
+ * }} opts
1011
+ * @returns {Promise<import('./types.js').NavigationFinished>}
1012
+ */
1013
+ async function load_root_error_page({ status, error, url, route }) {
1014
+ /** @type {Record<string, string>} */
1015
+ const params = {}; // error page does not have params
986
1016
 
987
- const default_layout_has_server_load = app.server_loads[0] === 0;
1017
+ /** @type {import('types').ServerDataNode | null} */
1018
+ let server_data_node = null;
988
1019
 
989
- if (default_layout_has_server_load) {
990
- // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
991
- // existing root layout data
992
- try {
993
- const server_data = await load_data(url, [true]);
1020
+ const default_layout_has_server_load = app.server_loads[0] === 0;
994
1021
 
995
- if (
996
- server_data.type !== 'data' ||
997
- (server_data.nodes[0] && server_data.nodes[0].type !== 'data')
998
- ) {
999
- throw 0;
1000
- }
1022
+ if (default_layout_has_server_load) {
1023
+ // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
1024
+ // existing root layout data
1025
+ try {
1026
+ const server_data = await load_data(url, [true]);
1001
1027
 
1002
- server_data_node = server_data.nodes[0] ?? null;
1003
- } catch {
1004
- // at this point we have no choice but to fall back to the server, if it wouldn't
1005
- // bring us right back here, turning this into an endless loop
1006
- if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
1007
- await native_navigation(url);
1008
- }
1028
+ if (
1029
+ server_data.type !== 'data' ||
1030
+ (server_data.nodes[0] && server_data.nodes[0].type !== 'data')
1031
+ ) {
1032
+ throw 0;
1033
+ }
1034
+
1035
+ server_data_node = server_data.nodes[0] ?? null;
1036
+ } catch {
1037
+ // at this point we have no choice but to fall back to the server, if it wouldn't
1038
+ // bring us right back here, turning this into an endless loop
1039
+ if (url.origin !== origin || url.pathname !== location.pathname || hydrated) {
1040
+ await native_navigation(url);
1009
1041
  }
1010
1042
  }
1043
+ }
1011
1044
 
1012
- const root_layout = await load_node({
1013
- loader: default_layout_loader,
1014
- url,
1015
- params,
1016
- route,
1017
- parent: () => Promise.resolve({}),
1018
- server_data_node: create_data_node(server_data_node)
1019
- });
1045
+ const root_layout = await load_node({
1046
+ loader: default_layout_loader,
1047
+ url,
1048
+ params,
1049
+ route,
1050
+ parent: () => Promise.resolve({}),
1051
+ server_data_node: create_data_node(server_data_node)
1052
+ });
1020
1053
 
1021
- /** @type {import('./types.js').BranchNode} */
1022
- const root_error = {
1023
- node: await default_error_loader(),
1024
- loader: default_error_loader,
1025
- universal: null,
1026
- server: null,
1027
- data: null
1028
- };
1054
+ /** @type {import('./types.js').BranchNode} */
1055
+ const root_error = {
1056
+ node: await default_error_loader(),
1057
+ loader: default_error_loader,
1058
+ universal: null,
1059
+ server: null,
1060
+ data: null
1061
+ };
1029
1062
 
1030
- return await get_navigation_result_from_branch({
1031
- url,
1032
- params,
1033
- branch: [root_layout, root_error],
1034
- status,
1035
- error,
1036
- route: null
1037
- });
1038
- }
1063
+ return await get_navigation_result_from_branch({
1064
+ url,
1065
+ params,
1066
+ branch: [root_layout, root_error],
1067
+ status,
1068
+ error,
1069
+ route: null
1070
+ });
1071
+ }
1039
1072
 
1040
- /**
1041
- * @param {URL} url
1042
- * @param {boolean} invalidating
1043
- */
1044
- function get_navigation_intent(url, invalidating) {
1045
- if (is_external_url(url, base)) return;
1073
+ /**
1074
+ * @param {URL} url
1075
+ * @param {boolean} invalidating
1076
+ */
1077
+ function get_navigation_intent(url, invalidating) {
1078
+ if (is_external_url(url, base)) return;
1046
1079
 
1047
- const path = get_url_path(url.pathname);
1080
+ const path = get_url_path(url.pathname);
1048
1081
 
1049
- for (const route of routes) {
1050
- const params = route.exec(path);
1082
+ for (const route of routes) {
1083
+ const params = route.exec(path);
1051
1084
 
1052
- if (params) {
1053
- const id = url.pathname + url.search;
1054
- /** @type {import('./types.js').NavigationIntent} */
1055
- const intent = { id, invalidating, route, params: decode_params(params), url };
1056
- return intent;
1057
- }
1085
+ if (params) {
1086
+ const id = url.pathname + url.search;
1087
+ /** @type {import('./types.js').NavigationIntent} */
1088
+ const intent = { id, invalidating, route, params: decode_params(params), url };
1089
+ return intent;
1058
1090
  }
1059
1091
  }
1092
+ }
1060
1093
 
1061
- /** @param {string} pathname */
1062
- function get_url_path(pathname) {
1063
- return decode_pathname(pathname.slice(base.length) || '/');
1064
- }
1094
+ /** @param {string} pathname */
1095
+ function get_url_path(pathname) {
1096
+ return decode_pathname(pathname.slice(base.length) || '/');
1097
+ }
1065
1098
 
1066
- /**
1067
- * @param {{
1068
- * url: URL;
1069
- * type: import('@sveltejs/kit').Navigation["type"];
1070
- * intent?: import('./types.js').NavigationIntent;
1071
- * delta?: number;
1072
- * }} opts
1073
- */
1074
- function before_navigate({ url, type, intent, delta }) {
1075
- let should_block = false;
1099
+ /**
1100
+ * @param {{
1101
+ * url: URL;
1102
+ * type: import('@sveltejs/kit').Navigation["type"];
1103
+ * intent?: import('./types.js').NavigationIntent;
1104
+ * delta?: number;
1105
+ * }} opts
1106
+ */
1107
+ function _before_navigate({ url, type, intent, delta }) {
1108
+ let should_block = false;
1076
1109
 
1077
- const nav = create_navigation(current, intent, url, type);
1110
+ const nav = create_navigation(current, intent, url, type);
1111
+
1112
+ if (delta !== undefined) {
1113
+ nav.navigation.delta = delta;
1114
+ }
1078
1115
 
1079
- if (delta !== undefined) {
1080
- nav.navigation.delta = delta;
1116
+ const cancellable = {
1117
+ ...nav.navigation,
1118
+ cancel: () => {
1119
+ should_block = true;
1120
+ nav.reject(new Error('navigation was cancelled'));
1081
1121
  }
1122
+ };
1082
1123
 
1083
- const cancellable = {
1084
- ...nav.navigation,
1085
- cancel: () => {
1086
- should_block = true;
1087
- nav.reject(new Error('navigation was cancelled'));
1088
- }
1089
- };
1124
+ if (!navigating) {
1125
+ // Don't run the event during redirects
1126
+ before_navigate_callbacks.forEach((fn) => fn(cancellable));
1127
+ }
1090
1128
 
1091
- if (!navigating) {
1092
- // Don't run the event during redirects
1093
- callbacks.before_navigate.forEach((fn) => fn(cancellable));
1094
- }
1129
+ return should_block ? null : nav;
1130
+ }
1095
1131
 
1096
- return should_block ? null : nav;
1132
+ /**
1133
+ * @param {{
1134
+ * type: import('@sveltejs/kit').Navigation["type"];
1135
+ * url: URL;
1136
+ * popped?: {
1137
+ * state: Record<string, any>;
1138
+ * scroll: { x: number, y: number };
1139
+ * delta: number;
1140
+ * };
1141
+ * keepfocus?: boolean;
1142
+ * noscroll?: boolean;
1143
+ * replace_state?: boolean;
1144
+ * state?: Record<string, any>;
1145
+ * redirect_count?: number;
1146
+ * nav_token?: {};
1147
+ * accept?: () => void;
1148
+ * block?: () => void;
1149
+ * }} opts
1150
+ */
1151
+ async function navigate({
1152
+ type,
1153
+ url,
1154
+ popped,
1155
+ keepfocus,
1156
+ noscroll,
1157
+ replace_state,
1158
+ state = {},
1159
+ redirect_count = 0,
1160
+ nav_token = {},
1161
+ accept = noop,
1162
+ block = noop
1163
+ }) {
1164
+ const intent = get_navigation_intent(url, false);
1165
+ const nav = _before_navigate({ url, type, delta: popped?.delta, intent });
1166
+
1167
+ if (!nav) {
1168
+ block();
1169
+ return;
1097
1170
  }
1098
1171
 
1099
- /**
1100
- * @param {{
1101
- * type: import('@sveltejs/kit').Navigation["type"];
1102
- * url: URL;
1103
- * popped?: {
1104
- * state: Record<string, any>;
1105
- * scroll: { x: number, y: number };
1106
- * delta: number;
1107
- * };
1108
- * keepfocus?: boolean;
1109
- * noscroll?: boolean;
1110
- * replace_state?: boolean;
1111
- * state?: Record<string, any>;
1112
- * redirect_count?: number;
1113
- * nav_token?: {};
1114
- * accept?: () => void;
1115
- * block?: () => void;
1116
- * }} opts
1117
- */
1118
- async function navigate({
1119
- type,
1120
- url,
1121
- popped,
1122
- keepfocus,
1123
- noscroll,
1124
- replace_state,
1125
- state = {},
1126
- redirect_count = 0,
1127
- nav_token = {},
1128
- accept = noop,
1129
- block = noop
1130
- }) {
1131
- const intent = get_navigation_intent(url, false);
1132
- const nav = before_navigate({ url, type, delta: popped?.delta, intent });
1133
-
1134
- if (!nav) {
1135
- block();
1136
- return;
1137
- }
1172
+ // store this before calling `accept()`, which may change the index
1173
+ const previous_history_index = current_history_index;
1174
+ const previous_navigation_index = current_navigation_index;
1175
+
1176
+ accept();
1138
1177
 
1139
- // store this before calling `accept()`, which may change the index
1140
- const previous_history_index = current_history_index;
1141
- const previous_navigation_index = current_navigation_index;
1178
+ navigating = true;
1142
1179
 
1143
- accept();
1180
+ if (started) {
1181
+ stores.navigating.set(nav.navigation);
1182
+ }
1144
1183
 
1145
- navigating = true;
1184
+ token = nav_token;
1185
+ let navigation_result = intent && (await load_route(intent));
1146
1186
 
1147
- if (started) {
1148
- stores.navigating.set(nav.navigation);
1187
+ if (!navigation_result) {
1188
+ if (is_external_url(url, base)) {
1189
+ return await native_navigation(url);
1149
1190
  }
1191
+ navigation_result = await server_fallback(
1192
+ url,
1193
+ { id: null },
1194
+ await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), {
1195
+ url,
1196
+ params: {},
1197
+ route: { id: null }
1198
+ }),
1199
+ 404
1200
+ );
1201
+ }
1150
1202
 
1151
- token = nav_token;
1152
- let navigation_result = intent && (await load_route(intent));
1203
+ // if this is an internal navigation intent, use the normalized
1204
+ // URL for the rest of the function
1205
+ url = intent?.url || url;
1153
1206
 
1154
- if (!navigation_result) {
1155
- if (is_external_url(url, base)) {
1156
- return await native_navigation(url);
1157
- }
1158
- navigation_result = await server_fallback(
1159
- url,
1160
- { id: null },
1161
- await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), {
1207
+ // abort if user navigated during update
1208
+ if (token !== nav_token) {
1209
+ nav.reject(new Error('navigation was aborted'));
1210
+ return false;
1211
+ }
1212
+
1213
+ if (navigation_result.type === 'redirect') {
1214
+ // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1215
+ if (redirect_count >= 20) {
1216
+ navigation_result = await load_root_error_page({
1217
+ status: 500,
1218
+ error: await handle_error(new Error('Redirect loop'), {
1162
1219
  url,
1163
1220
  params: {},
1164
1221
  route: { id: null }
1165
1222
  }),
1166
- 404
1167
- );
1168
- }
1169
-
1170
- // if this is an internal navigation intent, use the normalized
1171
- // URL for the rest of the function
1172
- url = intent?.url || url;
1173
-
1174
- // abort if user navigated during update
1175
- if (token !== nav_token) {
1176
- nav.reject(new Error('navigation was aborted'));
1223
+ url,
1224
+ route: { id: null }
1225
+ });
1226
+ } else {
1227
+ _goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token);
1177
1228
  return false;
1178
1229
  }
1179
-
1180
- if (navigation_result.type === 'redirect') {
1181
- // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
1182
- if (redirect_count >= 20) {
1183
- navigation_result = await load_root_error_page({
1184
- status: 500,
1185
- error: await handle_error(new Error('Redirect loop'), {
1186
- url,
1187
- params: {},
1188
- route: { id: null }
1189
- }),
1190
- url,
1191
- route: { id: null }
1192
- });
1193
- } else {
1194
- goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token);
1195
- return false;
1196
- }
1197
- } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1198
- const updated = await stores.updated.check();
1199
- if (updated) {
1200
- await native_navigation(url);
1201
- }
1230
+ } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
1231
+ const updated = await stores.updated.check();
1232
+ if (updated) {
1233
+ await native_navigation(url);
1202
1234
  }
1235
+ }
1203
1236
 
1204
- // reset invalidation only after a finished navigation. If there are redirects or
1205
- // additional invalidations, they should get the same invalidation treatment
1206
- invalidated.length = 0;
1207
- force_invalidation = false;
1237
+ // reset invalidation only after a finished navigation. If there are redirects or
1238
+ // additional invalidations, they should get the same invalidation treatment
1239
+ invalidated.length = 0;
1240
+ force_invalidation = false;
1208
1241
 
1209
- updating = true;
1242
+ updating = true;
1210
1243
 
1211
- update_scroll_positions(previous_history_index);
1212
- capture_snapshot(previous_navigation_index);
1244
+ update_scroll_positions(previous_history_index);
1245
+ capture_snapshot(previous_navigation_index);
1213
1246
 
1214
- // ensure the url pathname matches the page's trailing slash option
1215
- if (navigation_result.props.page.url.pathname !== url.pathname) {
1216
- url.pathname = navigation_result.props.page.url.pathname;
1217
- }
1247
+ // ensure the url pathname matches the page's trailing slash option
1248
+ if (navigation_result.props.page.url.pathname !== url.pathname) {
1249
+ url.pathname = navigation_result.props.page.url.pathname;
1250
+ }
1218
1251
 
1219
- state = popped ? popped.state : state;
1252
+ state = popped ? popped.state : state;
1220
1253
 
1221
- if (!popped) {
1222
- // this is a new navigation, rather than a popstate
1223
- const change = replace_state ? 0 : 1;
1254
+ if (!popped) {
1255
+ // this is a new navigation, rather than a popstate
1256
+ const change = replace_state ? 0 : 1;
1224
1257
 
1225
- const entry = {
1226
- [HISTORY_INDEX]: (current_history_index += change),
1227
- [NAVIGATION_INDEX]: (current_navigation_index += change)
1228
- };
1258
+ const entry = {
1259
+ [HISTORY_INDEX]: (current_history_index += change),
1260
+ [NAVIGATION_INDEX]: (current_navigation_index += change),
1261
+ [STATES_KEY]: state
1262
+ };
1229
1263
 
1230
- const fn = replace_state ? original_replace_state : original_push_state;
1231
- fn.call(history, entry, '', url);
1264
+ const fn = replace_state ? original_replace_state : original_push_state;
1265
+ fn.call(history, entry, '', url);
1232
1266
 
1233
- if (!replace_state) {
1234
- clear_onward_history(current_history_index, current_navigation_index);
1235
- }
1267
+ if (!replace_state) {
1268
+ clear_onward_history(current_history_index, current_navigation_index);
1236
1269
  }
1270
+ }
1237
1271
 
1238
- states[current_history_index] = state;
1239
-
1240
- // reset preload synchronously after the history state has been set to avoid race conditions
1241
- load_cache = null;
1272
+ // reset preload synchronously after the history state has been set to avoid race conditions
1273
+ load_cache = null;
1242
1274
 
1243
- navigation_result.props.page.state = state;
1275
+ navigation_result.props.page.state = state;
1244
1276
 
1245
- if (started) {
1246
- current = navigation_result.state;
1277
+ if (started) {
1278
+ current = navigation_result.state;
1247
1279
 
1248
- // reset url before updating page store
1249
- if (navigation_result.props.page) {
1250
- navigation_result.props.page.url = url;
1251
- }
1280
+ // reset url before updating page store
1281
+ if (navigation_result.props.page) {
1282
+ navigation_result.props.page.url = url;
1283
+ }
1252
1284
 
1253
- const after_navigate = (
1254
- await Promise.all(
1255
- callbacks.on_navigate.map((fn) =>
1256
- fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
1257
- )
1285
+ const after_navigate = (
1286
+ await Promise.all(
1287
+ on_navigate_callbacks.map((fn) =>
1288
+ fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
1258
1289
  )
1259
- ).filter((value) => typeof value === 'function');
1260
-
1261
- if (after_navigate.length > 0) {
1262
- function cleanup() {
1263
- callbacks.after_navigate = callbacks.after_navigate.filter(
1264
- // @ts-ignore
1265
- (fn) => !after_navigate.includes(fn)
1266
- );
1267
- }
1268
-
1269
- after_navigate.push(cleanup);
1290
+ )
1291
+ ).filter((value) => typeof value === 'function');
1270
1292
 
1271
- // @ts-ignore
1272
- callbacks.after_navigate.push(...after_navigate);
1293
+ if (after_navigate.length > 0) {
1294
+ function cleanup() {
1295
+ after_navigate_callbacks = after_navigate_callbacks.filter(
1296
+ // @ts-ignore
1297
+ (fn) => !after_navigate.includes(fn)
1298
+ );
1273
1299
  }
1274
1300
 
1275
- root.$set(navigation_result.props);
1276
- has_navigated = true;
1277
- } else {
1278
- initialize(navigation_result);
1301
+ after_navigate.push(cleanup);
1302
+
1303
+ // @ts-ignore
1304
+ callbacks.after_navigate.push(...after_navigate);
1279
1305
  }
1280
1306
 
1281
- const { activeElement } = document;
1307
+ root.$set(navigation_result.props);
1308
+ has_navigated = true;
1309
+ } else {
1310
+ initialize(navigation_result, target);
1311
+ }
1282
1312
 
1283
- // need to render the DOM before we can scroll to the rendered elements and do focus management
1284
- await tick();
1313
+ const { activeElement } = document;
1285
1314
 
1286
- // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
1287
- const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1288
-
1289
- if (autoscroll) {
1290
- const deep_linked =
1291
- url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
1292
- if (scroll) {
1293
- scrollTo(scroll.x, scroll.y);
1294
- } else if (deep_linked) {
1295
- // Here we use `scrollIntoView` on the element instead of `scrollTo`
1296
- // because it natively supports the `scroll-margin` and `scroll-behavior`
1297
- // CSS properties.
1298
- deep_linked.scrollIntoView();
1299
- } else {
1300
- scrollTo(0, 0);
1301
- }
1302
- }
1315
+ // need to render the DOM before we can scroll to the rendered elements and do focus management
1316
+ await tick();
1303
1317
 
1304
- const changed_focus =
1305
- // reset focus only if any manual focus management didn't override it
1306
- document.activeElement !== activeElement &&
1307
- // also refocus when activeElement is body already because the
1308
- // focus event might not have been fired on it yet
1309
- document.activeElement !== document.body;
1318
+ // we reset scroll before dealing with focus, to avoid a flash of unscrolled content
1319
+ const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null;
1310
1320
 
1311
- if (!keepfocus && !changed_focus) {
1312
- reset_focus();
1321
+ if (autoscroll) {
1322
+ const deep_linked = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
1323
+ if (scroll) {
1324
+ scrollTo(scroll.x, scroll.y);
1325
+ } else if (deep_linked) {
1326
+ // Here we use `scrollIntoView` on the element instead of `scrollTo`
1327
+ // because it natively supports the `scroll-margin` and `scroll-behavior`
1328
+ // CSS properties.
1329
+ deep_linked.scrollIntoView();
1330
+ } else {
1331
+ scrollTo(0, 0);
1313
1332
  }
1333
+ }
1314
1334
 
1315
- autoscroll = true;
1316
-
1317
- if (navigation_result.props.page) {
1318
- page = navigation_result.props.page;
1319
- }
1335
+ const changed_focus =
1336
+ // reset focus only if any manual focus management didn't override it
1337
+ document.activeElement !== activeElement &&
1338
+ // also refocus when activeElement is body already because the
1339
+ // focus event might not have been fired on it yet
1340
+ document.activeElement !== document.body;
1320
1341
 
1321
- navigating = false;
1342
+ if (!keepfocus && !changed_focus) {
1343
+ reset_focus();
1344
+ }
1322
1345
 
1323
- if (type === 'popstate') {
1324
- restore_snapshot(current_navigation_index);
1325
- }
1346
+ autoscroll = true;
1326
1347
 
1327
- nav.fulfil(undefined);
1348
+ if (navigation_result.props.page) {
1349
+ page = navigation_result.props.page;
1350
+ }
1328
1351
 
1329
- callbacks.after_navigate.forEach((fn) =>
1330
- fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
1331
- );
1332
- stores.navigating.set(null);
1352
+ navigating = false;
1333
1353
 
1334
- updating = false;
1354
+ if (type === 'popstate') {
1355
+ restore_snapshot(current_navigation_index);
1335
1356
  }
1336
1357
 
1337
- /**
1338
- * Does a full page reload if it wouldn't result in an endless loop in the SPA case
1339
- * @param {URL} url
1340
- * @param {{ id: string | null }} route
1341
- * @param {App.Error} error
1342
- * @param {number} status
1343
- * @returns {Promise<import('./types.js').NavigationFinished>}
1344
- */
1345
- async function server_fallback(url, route, error, status) {
1346
- if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
1347
- // We would reload the same page we're currently on, which isn't hydrated,
1348
- // which means no SSR, which means we would end up in an endless loop
1349
- return await load_root_error_page({
1350
- status,
1351
- error,
1352
- url,
1353
- route
1354
- });
1355
- }
1358
+ nav.fulfil(undefined);
1356
1359
 
1357
- if (DEV && status !== 404) {
1358
- console.error(
1359
- 'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
1360
- );
1360
+ after_navigate_callbacks.forEach((fn) =>
1361
+ fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
1362
+ );
1361
1363
 
1362
- debugger; // eslint-disable-line
1363
- }
1364
+ stores.navigating.set(null);
1364
1365
 
1365
- return await native_navigation(url);
1366
- }
1366
+ updating = false;
1367
+ }
1367
1368
 
1368
- if (import.meta.hot) {
1369
- import.meta.hot.on('vite:beforeUpdate', () => {
1370
- if (current.error) location.reload();
1369
+ /**
1370
+ * Does a full page reload if it wouldn't result in an endless loop in the SPA case
1371
+ * @param {URL} url
1372
+ * @param {{ id: string | null }} route
1373
+ * @param {App.Error} error
1374
+ * @param {number} status
1375
+ * @returns {Promise<import('./types.js').NavigationFinished>}
1376
+ */
1377
+ async function server_fallback(url, route, error, status) {
1378
+ if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
1379
+ // We would reload the same page we're currently on, which isn't hydrated,
1380
+ // which means no SSR, which means we would end up in an endless loop
1381
+ return await load_root_error_page({
1382
+ status,
1383
+ error,
1384
+ url,
1385
+ route
1371
1386
  });
1372
1387
  }
1373
1388
 
1374
- function setup_preload() {
1375
- /** @type {NodeJS.Timeout} */
1376
- let mousemove_timeout;
1389
+ if (DEV && status !== 404) {
1390
+ console.error(
1391
+ 'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)'
1392
+ );
1377
1393
 
1378
- container.addEventListener('mousemove', (event) => {
1379
- const target = /** @type {Element} */ (event.target);
1394
+ debugger; // eslint-disable-line
1395
+ }
1380
1396
 
1381
- clearTimeout(mousemove_timeout);
1382
- mousemove_timeout = setTimeout(() => {
1383
- preload(target, 2);
1384
- }, 20);
1385
- });
1397
+ return await native_navigation(url);
1398
+ }
1386
1399
 
1387
- /** @param {Event} event */
1388
- function tap(event) {
1389
- preload(/** @type {Element} */ (event.composedPath()[0]), 1);
1390
- }
1400
+ if (import.meta.hot) {
1401
+ import.meta.hot.on('vite:beforeUpdate', () => {
1402
+ if (current.error) location.reload();
1403
+ });
1404
+ }
1391
1405
 
1392
- container.addEventListener('mousedown', tap);
1393
- container.addEventListener('touchstart', tap, { passive: true });
1406
+ function setup_preload() {
1407
+ /** @type {NodeJS.Timeout} */
1408
+ let mousemove_timeout;
1394
1409
 
1395
- const observer = new IntersectionObserver(
1396
- (entries) => {
1397
- for (const entry of entries) {
1398
- if (entry.isIntersecting) {
1399
- preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href);
1400
- observer.unobserve(entry.target);
1401
- }
1402
- }
1403
- },
1404
- { threshold: 0 }
1405
- );
1410
+ container.addEventListener('mousemove', (event) => {
1411
+ const target = /** @type {Element} */ (event.target);
1406
1412
 
1407
- /**
1408
- * @param {Element} element
1409
- * @param {number} priority
1410
- */
1411
- function preload(element, priority) {
1412
- const a = find_anchor(element, container);
1413
- if (!a) return;
1413
+ clearTimeout(mousemove_timeout);
1414
+ mousemove_timeout = setTimeout(() => {
1415
+ preload(target, 2);
1416
+ }, 20);
1417
+ });
1414
1418
 
1415
- const { url, external, download } = get_link_info(a, base);
1416
- if (external || download) return;
1419
+ /** @param {Event} event */
1420
+ function tap(event) {
1421
+ preload(/** @type {Element} */ (event.composedPath()[0]), 1);
1422
+ }
1417
1423
 
1418
- const options = get_router_options(a);
1424
+ container.addEventListener('mousedown', tap);
1425
+ container.addEventListener('touchstart', tap, { passive: true });
1419
1426
 
1420
- if (!options.reload) {
1421
- if (priority <= options.preload_data) {
1422
- const intent = get_navigation_intent(/** @type {URL} */ (url), false);
1423
- if (intent) {
1424
- if (DEV) {
1425
- preload_data(intent).then((result) => {
1426
- if (result.type === 'loaded' && result.state.error) {
1427
- console.warn(
1428
- `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` +
1429
- 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
1430
- 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
1431
- 'See https://kit.svelte.dev/docs/link-options for more info'
1432
- );
1433
- }
1434
- });
1435
- } else {
1436
- preload_data(intent);
1437
- }
1427
+ const observer = new IntersectionObserver(
1428
+ (entries) => {
1429
+ for (const entry of entries) {
1430
+ if (entry.isIntersecting) {
1431
+ _preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href);
1432
+ observer.unobserve(entry.target);
1433
+ }
1434
+ }
1435
+ },
1436
+ { threshold: 0 }
1437
+ );
1438
+
1439
+ /**
1440
+ * @param {Element} element
1441
+ * @param {number} priority
1442
+ */
1443
+ function preload(element, priority) {
1444
+ const a = find_anchor(element, container);
1445
+ if (!a) return;
1446
+
1447
+ const { url, external, download } = get_link_info(a, base);
1448
+ if (external || download) return;
1449
+
1450
+ const options = get_router_options(a);
1451
+
1452
+ if (!options.reload) {
1453
+ if (priority <= options.preload_data) {
1454
+ const intent = get_navigation_intent(/** @type {URL} */ (url), false);
1455
+ if (intent) {
1456
+ if (DEV) {
1457
+ _preload_data(intent).then((result) => {
1458
+ if (result.type === 'loaded' && result.state.error) {
1459
+ console.warn(
1460
+ `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` +
1461
+ 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' +
1462
+ 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' +
1463
+ 'See https://kit.svelte.dev/docs/link-options for more info'
1464
+ );
1465
+ }
1466
+ });
1467
+ } else {
1468
+ _preload_data(intent);
1438
1469
  }
1439
- } else if (priority <= options.preload_code) {
1440
- preload_code(/** @type {URL} */ (url).pathname);
1441
1470
  }
1471
+ } else if (priority <= options.preload_code) {
1472
+ _preload_code(/** @type {URL} */ (url).pathname);
1442
1473
  }
1443
1474
  }
1475
+ }
1444
1476
 
1445
- function after_navigate() {
1446
- observer.disconnect();
1477
+ function after_navigate() {
1478
+ observer.disconnect();
1447
1479
 
1448
- for (const a of container.querySelectorAll('a')) {
1449
- const { url, external, download } = get_link_info(a, base);
1450
- if (external || download) continue;
1480
+ for (const a of container.querySelectorAll('a')) {
1481
+ const { url, external, download } = get_link_info(a, base);
1482
+ if (external || download) continue;
1451
1483
 
1452
- const options = get_router_options(a);
1453
- if (options.reload) continue;
1484
+ const options = get_router_options(a);
1485
+ if (options.reload) continue;
1454
1486
 
1455
- if (options.preload_code === PRELOAD_PRIORITIES.viewport) {
1456
- observer.observe(a);
1457
- }
1487
+ if (options.preload_code === PRELOAD_PRIORITIES.viewport) {
1488
+ observer.observe(a);
1489
+ }
1458
1490
 
1459
- if (options.preload_code === PRELOAD_PRIORITIES.eager) {
1460
- preload_code(/** @type {URL} */ (url).pathname);
1461
- }
1491
+ if (options.preload_code === PRELOAD_PRIORITIES.eager) {
1492
+ _preload_code(/** @type {URL} */ (url).pathname);
1462
1493
  }
1463
1494
  }
1464
-
1465
- callbacks.after_navigate.push(after_navigate);
1466
- after_navigate();
1467
1495
  }
1468
1496
 
1469
- /**
1470
- * @param {unknown} error
1471
- * @param {import('@sveltejs/kit').NavigationEvent} event
1472
- * @returns {import('types').MaybePromise<App.Error>}
1473
- */
1474
- function handle_error(error, event) {
1475
- if (error instanceof HttpError) {
1476
- return error.body;
1477
- }
1478
-
1479
- if (DEV) {
1480
- errored = true;
1481
- console.warn('The next HMR update will cause the page to reload');
1482
- }
1497
+ after_navigate_callbacks.push(after_navigate);
1498
+ after_navigate();
1499
+ }
1483
1500
 
1484
- const status = get_status(error);
1485
- const message = get_message(error);
1501
+ /**
1502
+ * @param {unknown} error
1503
+ * @param {import('@sveltejs/kit').NavigationEvent} event
1504
+ * @returns {import('types').MaybePromise<App.Error>}
1505
+ */
1506
+ function handle_error(error, event) {
1507
+ if (error instanceof HttpError) {
1508
+ return error.body;
1509
+ }
1486
1510
 
1487
- return (
1488
- app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
1489
- );
1511
+ if (DEV) {
1512
+ errored = true;
1513
+ console.warn('The next HMR update will cause the page to reload');
1490
1514
  }
1491
1515
 
1492
- return {
1493
- after_navigate: (fn) => {
1494
- onMount(() => {
1495
- callbacks.after_navigate.push(fn);
1516
+ const status = get_status(error);
1517
+ const message = get_message(error);
1496
1518
 
1497
- return () => {
1498
- const i = callbacks.after_navigate.indexOf(fn);
1499
- callbacks.after_navigate.splice(i, 1);
1500
- };
1501
- });
1502
- },
1519
+ return (
1520
+ app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
1521
+ );
1522
+ }
1503
1523
 
1504
- before_navigate: (fn) => {
1505
- onMount(() => {
1506
- callbacks.before_navigate.push(fn);
1524
+ /**
1525
+ * @template {Function} T
1526
+ * @param {T[]} callbacks
1527
+ * @param {T} callback
1528
+ */
1529
+ function add_navigation_callback(callbacks, callback) {
1530
+ onMount(() => {
1531
+ callbacks.push(callback);
1507
1532
 
1508
- return () => {
1509
- const i = callbacks.before_navigate.indexOf(fn);
1510
- callbacks.before_navigate.splice(i, 1);
1511
- };
1512
- });
1513
- },
1533
+ return () => {
1534
+ const i = callbacks.indexOf(callback);
1535
+ callbacks.splice(i, 1);
1536
+ };
1537
+ });
1538
+ }
1514
1539
 
1515
- on_navigate: (fn) => {
1516
- onMount(() => {
1517
- callbacks.on_navigate.push(fn);
1540
+ /**
1541
+ * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL.
1542
+ *
1543
+ * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
1544
+ * @param {(navigation: import('@sveltejs/kit').AfterNavigate) => void} callback
1545
+ * @returns {void}
1546
+ */
1547
+ export function afterNavigate(callback) {
1548
+ add_navigation_callback(after_navigate_callbacks, callback);
1549
+ }
1518
1550
 
1519
- return () => {
1520
- const i = callbacks.on_navigate.indexOf(fn);
1521
- callbacks.on_navigate.splice(i, 1);
1522
- };
1523
- });
1524
- },
1551
+ /**
1552
+ * A navigation interceptor that triggers before we navigate to a new URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls.
1553
+ *
1554
+ * Calling `cancel()` will prevent the navigation from completing. If `navigation.type === 'leave'` — meaning the user is navigating away from the app (or closing the tab) — calling `cancel` will trigger the native browser unload confirmation dialog. In this case, the navigation may or may not be cancelled depending on the user's response.
1555
+ *
1556
+ * When a navigation isn't to a SvelteKit-owned route (and therefore controlled by SvelteKit's client-side router), `navigation.to.route.id` will be `null`.
1557
+ *
1558
+ * If the navigation will (if not cancelled) cause the document to unload — in other words `'leave'` navigations and `'link'` navigations where `navigation.to.route === null` — `navigation.willUnload` is `true`.
1559
+ *
1560
+ * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
1561
+ * @param {(navigation: import('@sveltejs/kit').BeforeNavigate) => void} callback
1562
+ * @returns {void}
1563
+ */
1564
+ export function beforeNavigate(callback) {
1565
+ add_navigation_callback(before_navigate_callbacks, callback);
1566
+ }
1525
1567
 
1526
- disable_scroll_handling: () => {
1527
- if (DEV && started && !updating) {
1528
- throw new Error('Can only disable scroll handling during navigation');
1529
- }
1568
+ /**
1569
+ * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations.
1570
+ *
1571
+ * If you return a `Promise`, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use `document.startViewTransition`. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.
1572
+ *
1573
+ * If a function (or a `Promise` that resolves to a function) is returned from the callback, it will be called once the DOM has updated.
1574
+ *
1575
+ * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
1576
+ * @param {(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<void>} callback
1577
+ * @returns {void}
1578
+ */
1579
+ export function onNavigate(callback) {
1580
+ add_navigation_callback(on_navigate_callbacks, callback);
1581
+ }
1530
1582
 
1531
- if (updating || !started) {
1532
- autoscroll = false;
1533
- }
1534
- },
1583
+ /**
1584
+ * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling.
1585
+ * This is generally discouraged, since it breaks user expectations.
1586
+ * @returns {void}
1587
+ */
1588
+ export function disableScrollHandling() {
1589
+ if (!BROWSER) {
1590
+ throw new Error('Cannot call disableScrollHandling() on the server');
1591
+ }
1535
1592
 
1536
- goto: (url, opts = {}) => {
1537
- url = resolve_url(url);
1593
+ if (DEV && started && !updating) {
1594
+ throw new Error('Can only disable scroll handling during navigation');
1595
+ }
1538
1596
 
1539
- if (url.origin !== origin) {
1540
- return Promise.reject(
1541
- new Error(
1542
- DEV
1543
- ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
1544
- : 'goto: invalid URL'
1545
- )
1546
- );
1547
- }
1597
+ if (updating || !started) {
1598
+ autoscroll = false;
1599
+ }
1600
+ }
1548
1601
 
1549
- return goto(url, opts, 0);
1550
- },
1602
+ /**
1603
+ * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`.
1604
+ * For external URLs, use `window.location = url` instead of calling `goto(url)`.
1605
+ *
1606
+ * @param {string | URL} url Where to navigate to. Note that if you've set [`config.kit.paths.base`](https://kit.svelte.dev/docs/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app.
1607
+ * @param {Object} [opts] Options related to the navigation
1608
+ * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
1609
+ * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation
1610
+ * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body
1611
+ * @param {boolean} [opts.invalidateAll] If `true`, all `load` functions of the page will be rerun. See https://kit.svelte.dev/docs/load#rerunning-load-functions for more info on invalidation.
1612
+ * @param {App.PageState} [opts.state] An optional object that will be available on the `$page.state` store
1613
+ * @returns {Promise<void>}
1614
+ */
1615
+ export function goto(url, opts = {}) {
1616
+ if (!BROWSER) {
1617
+ throw new Error('Cannot call goto(...) on the server');
1618
+ }
1551
1619
 
1552
- invalidate: (resource) => {
1553
- if (typeof resource === 'function') {
1554
- invalidated.push(resource);
1555
- } else {
1556
- const { href } = new URL(resource, location.href);
1557
- invalidated.push((url) => url.href === href);
1558
- }
1620
+ url = resolve_url(url);
1559
1621
 
1560
- return invalidate();
1561
- },
1622
+ if (url.origin !== origin) {
1623
+ return Promise.reject(
1624
+ new Error(
1625
+ DEV
1626
+ ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead`
1627
+ : 'goto: invalid URL'
1628
+ )
1629
+ );
1630
+ }
1562
1631
 
1563
- invalidate_all: () => {
1564
- force_invalidation = true;
1565
- return invalidate();
1566
- },
1632
+ return _goto(url, opts, 0);
1633
+ }
1567
1634
 
1568
- preload_data: async (href) => {
1569
- const url = resolve_url(href);
1570
- const intent = get_navigation_intent(url, false);
1635
+ /**
1636
+ * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated.
1637
+ *
1638
+ * If the argument is given as a `string` or `URL`, it must resolve to the same URL that was passed to `fetch` or `depends` (including query parameters).
1639
+ * To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL.
1640
+ *
1641
+ * The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned.
1642
+ * This can be useful if you want to invalidate based on a pattern instead of a exact match.
1643
+ *
1644
+ * ```ts
1645
+ * // Example: Match '/path' regardless of the query parameters
1646
+ * import { invalidate } from '$app/navigation';
1647
+ *
1648
+ * invalidate((url) => url.pathname === '/path');
1649
+ * ```
1650
+ * @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
1651
+ * @returns {Promise<void>}
1652
+ */
1653
+ export function invalidate(resource) {
1654
+ if (!BROWSER) {
1655
+ throw new Error('Cannot call invalidate(...) on the server');
1656
+ }
1571
1657
 
1572
- if (!intent) {
1573
- throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
1574
- }
1658
+ if (typeof resource === 'function') {
1659
+ invalidated.push(resource);
1660
+ } else {
1661
+ const { href } = new URL(resource, location.href);
1662
+ invalidated.push((url) => url.href === href);
1663
+ }
1575
1664
 
1576
- const result = await preload_data(intent);
1577
- if (result.type === 'redirect') {
1578
- return {
1579
- type: result.type,
1580
- location: result.location
1581
- };
1582
- }
1665
+ return _invalidate();
1666
+ }
1583
1667
 
1584
- const { status, data } = result.props.page ?? page;
1585
- return { type: result.type, status, data };
1586
- },
1668
+ /**
1669
+ * Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
1670
+ * @returns {Promise<void>}
1671
+ */
1672
+ export function invalidateAll() {
1673
+ if (!BROWSER) {
1674
+ throw new Error('Cannot call invalidateAll() on the server');
1675
+ }
1587
1676
 
1588
- preload_code: (pathname) => {
1589
- if (DEV) {
1590
- if (!pathname.startsWith(base)) {
1591
- throw new Error(
1592
- `pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")`
1593
- );
1594
- }
1677
+ force_invalidation = true;
1678
+ return _invalidate();
1679
+ }
1595
1680
 
1596
- if (!routes.find((route) => route.exec(get_url_path(pathname)))) {
1597
- throw new Error(`'${pathname}' did not match any routes`);
1598
- }
1599
- }
1681
+ /**
1682
+ * Programmatically preloads the given page, which means
1683
+ * 1. ensuring that the code for the page is loaded, and
1684
+ * 2. calling the page's load function with the appropriate options.
1685
+ *
1686
+ * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `<a>` element with `data-sveltekit-preload-data`.
1687
+ * If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous.
1688
+ * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
1689
+ *
1690
+ * @param {string} href Page to preload
1691
+ * @returns {Promise<{ type: 'loaded'; status: number; data: Record<string, any> } | { type: 'redirect'; location: string }>}
1692
+ */
1693
+ export async function preloadData(href) {
1694
+ if (!BROWSER) {
1695
+ throw new Error('Cannot call preloadData(...) on the server');
1696
+ }
1600
1697
 
1601
- return preload_code(pathname);
1602
- },
1698
+ const url = resolve_url(href);
1699
+ const intent = get_navigation_intent(url, false);
1603
1700
 
1604
- push_state: (url, state) => {
1605
- if (DEV) {
1606
- try {
1607
- devalue.stringify(state);
1608
- } catch (error) {
1609
- // @ts-expect-error
1610
- throw new Error(`Could not serialize state${error.path}`);
1611
- }
1612
- }
1701
+ if (!intent) {
1702
+ throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
1703
+ }
1613
1704
 
1614
- update_scroll_positions(current_history_index);
1615
- const opts = {
1616
- [HISTORY_INDEX]: (current_history_index += 1),
1617
- [NAVIGATION_INDEX]: current_navigation_index,
1618
- [PAGE_URL_KEY]: page.url.href
1619
- };
1705
+ const result = await _preload_data(intent);
1706
+ if (result.type === 'redirect') {
1707
+ return {
1708
+ type: result.type,
1709
+ location: result.location
1710
+ };
1711
+ }
1620
1712
 
1621
- original_push_state.call(history, opts, '', resolve_url(url));
1713
+ const { status, data } = result.props.page ?? page;
1714
+ return { type: result.type, status, data };
1715
+ }
1622
1716
 
1623
- page = { ...page, state };
1624
- root.$set({ page });
1717
+ /**
1718
+ * Programmatically imports the code for routes that haven't yet been fetched.
1719
+ * Typically, you might call this to speed up subsequent navigation.
1720
+ *
1721
+ * You can specify routes by any matching pathname such as `/about` (to match `src/routes/about/+page.svelte`) or `/blog/*` (to match `src/routes/blog/[slug]/+page.svelte`).
1722
+ *
1723
+ * Unlike `preloadData`, this won't call `load` functions.
1724
+ * Returns a Promise that resolves when the modules have been imported.
1725
+ *
1726
+ * @param {string} pathname
1727
+ * @returns {Promise<void>}
1728
+ */
1729
+ export function preloadCode(pathname) {
1730
+ if (!BROWSER) {
1731
+ throw new Error('Cannot call preloadCode(...) on the server');
1732
+ }
1625
1733
 
1626
- states[current_history_index] = state;
1627
- clear_onward_history(current_history_index, current_navigation_index);
1628
- },
1734
+ if (DEV) {
1735
+ if (!pathname.startsWith(base)) {
1736
+ throw new Error(
1737
+ `pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")`
1738
+ );
1739
+ }
1629
1740
 
1630
- replace_state: (url, state) => {
1631
- if (DEV) {
1632
- try {
1633
- devalue.stringify(state);
1634
- } catch (error) {
1635
- // @ts-expect-error
1636
- throw new Error(`Could not serialize state${error.path}`);
1637
- }
1638
- }
1741
+ if (!routes.find((route) => route.exec(get_url_path(pathname)))) {
1742
+ throw new Error(`'${pathname}' did not match any routes`);
1743
+ }
1744
+ }
1639
1745
 
1640
- const opts = {
1641
- [HISTORY_INDEX]: current_history_index,
1642
- [NAVIGATION_INDEX]: current_navigation_index,
1643
- [PAGE_URL_KEY]: page.url.href
1644
- };
1746
+ return _preload_code(pathname);
1747
+ }
1645
1748
 
1646
- original_replace_state.call(history, opts, '', resolve_url(url));
1749
+ /**
1750
+ * Programmatically create a new history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://kit.svelte.dev/docs/shallow-routing).
1751
+ *
1752
+ * @param {string | URL} url
1753
+ * @param {App.PageState} state
1754
+ * @returns {void}
1755
+ */
1756
+ export function pushState(url, state) {
1757
+ if (!BROWSER) {
1758
+ throw new Error('Cannot call pushState(...) on the server');
1759
+ }
1647
1760
 
1648
- page = { ...page, state };
1649
- root.$set({ page });
1761
+ if (DEV) {
1762
+ try {
1763
+ // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
1764
+ devalue.stringify(state);
1765
+ } catch (error) {
1766
+ // @ts-expect-error
1767
+ throw new Error(`Could not serialize state${error.path}`);
1768
+ }
1769
+ }
1650
1770
 
1651
- states[current_history_index] = state;
1652
- },
1771
+ update_scroll_positions(current_history_index);
1653
1772
 
1654
- apply_action: async (result) => {
1655
- if (result.type === 'error') {
1656
- const url = new URL(location.href);
1773
+ const opts = {
1774
+ [HISTORY_INDEX]: (current_history_index += 1),
1775
+ [NAVIGATION_INDEX]: current_navigation_index,
1776
+ [PAGE_URL_KEY]: page.url.href,
1777
+ [STATES_KEY]: state
1778
+ };
1657
1779
 
1658
- const { branch, route } = current;
1659
- if (!route) return;
1780
+ original_push_state.call(history, opts, '', resolve_url(url));
1660
1781
 
1661
- const error_load = await load_nearest_error_page(
1662
- current.branch.length,
1663
- branch,
1664
- route.errors
1665
- );
1666
- if (error_load) {
1667
- const navigation_result = await get_navigation_result_from_branch({
1668
- url,
1669
- params: current.params,
1670
- branch: branch.slice(0, error_load.idx).concat(error_load.node),
1671
- status: result.status ?? 500,
1672
- error: result.error,
1673
- route
1674
- });
1782
+ page = { ...page, state };
1783
+ root.$set({ page });
1784
+
1785
+ clear_onward_history(current_history_index, current_navigation_index);
1786
+ }
1787
+
1788
+ /**
1789
+ * Programmatically replace the current history entry with the given `$page.state`. To use the current URL, you can pass `''` as the first argument. Used for [shallow routing](https://kit.svelte.dev/docs/shallow-routing).
1790
+ *
1791
+ * @param {string | URL} url
1792
+ * @param {App.PageState} state
1793
+ * @returns {void}
1794
+ */
1795
+ export function replaceState(url, state) {
1796
+ if (!BROWSER) {
1797
+ throw new Error('Cannot call replaceState(...) on the server');
1798
+ }
1675
1799
 
1676
- current = navigation_result.state;
1800
+ if (DEV) {
1801
+ try {
1802
+ // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances
1803
+ devalue.stringify(state);
1804
+ } catch (error) {
1805
+ // @ts-expect-error
1806
+ throw new Error(`Could not serialize state${error.path}`);
1807
+ }
1808
+ }
1677
1809
 
1678
- root.$set(navigation_result.props);
1810
+ const opts = {
1811
+ [HISTORY_INDEX]: current_history_index,
1812
+ [NAVIGATION_INDEX]: current_navigation_index,
1813
+ [PAGE_URL_KEY]: page.url.href,
1814
+ [STATES_KEY]: state
1815
+ };
1679
1816
 
1680
- tick().then(reset_focus);
1681
- }
1682
- } else if (result.type === 'redirect') {
1683
- goto(result.location, { invalidateAll: true }, 0);
1684
- } else {
1685
- /** @type {Record<string, any>} */
1686
- root.$set({
1687
- // this brings Svelte's view of the world in line with SvelteKit's
1688
- // after use:enhance reset the form....
1689
- form: null,
1690
- page: { ...page, form: result.data, status: result.status }
1691
- });
1692
-
1693
- // ...so that setting the `form` prop takes effect and isn't ignored
1694
- await tick();
1695
- root.$set({ form: result.data });
1696
-
1697
- if (result.type === 'success') {
1698
- reset_focus();
1699
- }
1700
- }
1701
- },
1817
+ original_replace_state.call(history, opts, '', resolve_url(url));
1702
1818
 
1703
- _start_router: () => {
1704
- history.scrollRestoration = 'manual';
1705
-
1706
- // Adopted from Nuxt.js
1707
- // Reset scrollRestoration to auto when leaving page, allowing page reload
1708
- // and back-navigation from other pages to use the browser to restore the
1709
- // scrolling position.
1710
- addEventListener('beforeunload', (e) => {
1711
- let should_block = false;
1712
-
1713
- persist_state();
1714
-
1715
- if (!navigating) {
1716
- const nav = create_navigation(current, undefined, null, 'leave');
1717
-
1718
- // If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
1719
- // it's due to an external or full-page-reload link, for which we don't want to call the hook again.
1720
- /** @type {import('@sveltejs/kit').BeforeNavigate} */
1721
- const navigation = {
1722
- ...nav.navigation,
1723
- cancel: () => {
1724
- should_block = true;
1725
- nav.reject(new Error('navigation was cancelled'));
1726
- }
1727
- };
1819
+ page = { ...page, state };
1820
+ root.$set({ page });
1821
+ }
1728
1822
 
1729
- callbacks.before_navigate.forEach((fn) => fn(navigation));
1730
- }
1823
+ /**
1824
+ * This action updates the `form` property of the current page with the given data and updates `$page.status`.
1825
+ * In case of an error, it redirects to the nearest error page.
1826
+ * @template {Record<string, unknown> | undefined} Success
1827
+ * @template {Record<string, unknown> | undefined} Failure
1828
+ * @param {import('@sveltejs/kit').ActionResult<Success, Failure>} result
1829
+ * @returns {Promise<void>}
1830
+ */
1831
+ export async function applyAction(result) {
1832
+ if (!BROWSER) {
1833
+ throw new Error('Cannot call applyAction(...) on the server');
1834
+ }
1731
1835
 
1732
- if (should_block) {
1733
- e.preventDefault();
1734
- e.returnValue = '';
1735
- } else {
1736
- history.scrollRestoration = 'auto';
1737
- }
1738
- });
1836
+ if (result.type === 'error') {
1837
+ const url = new URL(location.href);
1739
1838
 
1740
- addEventListener('visibilitychange', () => {
1741
- if (document.visibilityState === 'hidden') {
1742
- persist_state();
1743
- }
1839
+ const { branch, route } = current;
1840
+ if (!route) return;
1841
+
1842
+ const error_load = await load_nearest_error_page(current.branch.length, branch, route.errors);
1843
+ if (error_load) {
1844
+ const navigation_result = await get_navigation_result_from_branch({
1845
+ url,
1846
+ params: current.params,
1847
+ branch: branch.slice(0, error_load.idx).concat(error_load.node),
1848
+ status: result.status ?? 500,
1849
+ error: result.error,
1850
+ route
1744
1851
  });
1745
1852
 
1746
- // @ts-expect-error this isn't supported everywhere yet
1747
- if (!navigator.connection?.saveData) {
1748
- setup_preload();
1749
- }
1853
+ current = navigation_result.state;
1750
1854
 
1751
- /** @param {MouseEvent} event */
1752
- container.addEventListener('click', (event) => {
1753
- // Adapted from https://github.com/visionmedia/page.js
1754
- // MIT license https://github.com/visionmedia/page.js#license
1755
- if (event.button || event.which !== 1) return;
1756
- if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
1757
- if (event.defaultPrevented) return;
1758
-
1759
- const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
1760
- if (!a) return;
1761
-
1762
- const { url, external, target, download } = get_link_info(a, base);
1763
- if (!url) return;
1764
-
1765
- // bail out before `beforeNavigate` if link opens in a different tab
1766
- if (target === '_parent' || target === '_top') {
1767
- if (window.parent !== window) return;
1768
- } else if (target && target !== '_self') {
1769
- return;
1770
- }
1855
+ root.$set(navigation_result.props);
1771
1856
 
1772
- const options = get_router_options(a);
1773
- const is_svg_a_element = a instanceof SVGAElement;
1774
-
1775
- // Ignore URL protocols that differ to the current one and are not http(s) (e.g. `mailto:`, `tel:`, `myapp:`, etc.)
1776
- // This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external
1777
- // navigation, but it's not clear how to handle that case and it's not likely to come up in practice.
1778
- // MEMO: Without this condition, firefox will open mailer twice.
1779
- // See:
1780
- // - https://github.com/sveltejs/kit/issues/4045
1781
- // - https://github.com/sveltejs/kit/issues/5725
1782
- // - https://github.com/sveltejs/kit/issues/6496
1783
- if (
1784
- !is_svg_a_element &&
1785
- url.protocol !== location.protocol &&
1786
- !(url.protocol === 'https:' || url.protocol === 'http:')
1787
- )
1788
- return;
1857
+ tick().then(reset_focus);
1858
+ }
1859
+ } else if (result.type === 'redirect') {
1860
+ _goto(result.location, { invalidateAll: true }, 0);
1861
+ } else {
1862
+ /** @type {Record<string, any>} */
1863
+ root.$set({
1864
+ // this brings Svelte's view of the world in line with SvelteKit's
1865
+ // after use:enhance reset the form....
1866
+ form: null,
1867
+ page: { ...page, form: result.data, status: result.status }
1868
+ });
1789
1869
 
1790
- if (download) return;
1870
+ // ...so that setting the `form` prop takes effect and isn't ignored
1871
+ await tick();
1872
+ root.$set({ form: result.data });
1791
1873
 
1792
- // Ignore the following but fire beforeNavigate
1793
- if (external || options.reload) {
1794
- if (before_navigate({ url, type: 'link' })) {
1795
- // set `navigating` to `true` to prevent `beforeNavigate` callbacks
1796
- // being called when the page unloads
1797
- navigating = true;
1798
- } else {
1799
- event.preventDefault();
1800
- }
1874
+ if (result.type === 'success') {
1875
+ reset_focus();
1876
+ }
1877
+ }
1878
+ }
1879
+
1880
+ function _start_router() {
1881
+ history.scrollRestoration = 'manual';
1882
+
1883
+ // Adopted from Nuxt.js
1884
+ // Reset scrollRestoration to auto when leaving page, allowing page reload
1885
+ // and back-navigation from other pages to use the browser to restore the
1886
+ // scrolling position.
1887
+ addEventListener('beforeunload', (e) => {
1888
+ let should_block = false;
1889
+
1890
+ persist_state();
1801
1891
 
1802
- return;
1892
+ if (!navigating) {
1893
+ const nav = create_navigation(current, undefined, null, 'leave');
1894
+
1895
+ // If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
1896
+ // it's due to an external or full-page-reload link, for which we don't want to call the hook again.
1897
+ /** @type {import('@sveltejs/kit').BeforeNavigate} */
1898
+ const navigation = {
1899
+ ...nav.navigation,
1900
+ cancel: () => {
1901
+ should_block = true;
1902
+ nav.reject(new Error('navigation was cancelled'));
1803
1903
  }
1904
+ };
1804
1905
 
1805
- // Check if new url only differs by hash and use the browser default behavior in that case
1806
- // This will ensure the `hashchange` event is fired
1807
- // Removing the hash does a full page navigation in the browser, so make sure a hash is present
1808
- const [nonhash, hash] = url.href.split('#');
1809
- if (hash !== undefined && nonhash === strip_hash(location)) {
1810
- // If we are trying to navigate to the same hash, we should only
1811
- // attempt to scroll to that element and avoid any history changes.
1812
- // Otherwise, this can cause Firefox to incorrectly assign a null
1813
- // history state value without any signal that we can detect.
1814
- const [, current_hash] = current.url.href.split('#');
1815
- if (current_hash === hash) {
1816
- event.preventDefault();
1817
-
1818
- // We're already on /# and click on a link that goes to /#, or we're on
1819
- // /#top and click on a link that goes to /#top. In those cases just go to
1820
- // the top of the page, and avoid a history change.
1821
- if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) {
1822
- window.scrollTo({ top: 0 });
1823
- } else {
1824
- a.ownerDocument.getElementById(hash)?.scrollIntoView();
1825
- }
1906
+ before_navigate_callbacks.forEach((fn) => fn(navigation));
1907
+ }
1826
1908
 
1827
- return;
1828
- }
1829
- // set this flag to distinguish between navigations triggered by
1830
- // clicking a hash link and those triggered by popstate
1831
- hash_navigating = true;
1909
+ if (should_block) {
1910
+ e.preventDefault();
1911
+ e.returnValue = '';
1912
+ } else {
1913
+ history.scrollRestoration = 'auto';
1914
+ }
1915
+ });
1832
1916
 
1833
- update_scroll_positions(current_history_index);
1917
+ addEventListener('visibilitychange', () => {
1918
+ if (document.visibilityState === 'hidden') {
1919
+ persist_state();
1920
+ }
1921
+ });
1834
1922
 
1835
- update_url(url);
1923
+ // @ts-expect-error this isn't supported everywhere yet
1924
+ if (!navigator.connection?.saveData) {
1925
+ setup_preload();
1926
+ }
1836
1927
 
1837
- if (!options.replace_state) return;
1928
+ /** @param {MouseEvent} event */
1929
+ container.addEventListener('click', (event) => {
1930
+ // Adapted from https://github.com/visionmedia/page.js
1931
+ // MIT license https://github.com/visionmedia/page.js#license
1932
+ if (event.button || event.which !== 1) return;
1933
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
1934
+ if (event.defaultPrevented) return;
1838
1935
 
1839
- // hashchange event shouldn't occur if the router is replacing state.
1840
- hash_navigating = false;
1841
- }
1936
+ const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
1937
+ if (!a) return;
1938
+
1939
+ const { url, external, target, download } = get_link_info(a, base);
1940
+ if (!url) return;
1941
+
1942
+ // bail out before `beforeNavigate` if link opens in a different tab
1943
+ if (target === '_parent' || target === '_top') {
1944
+ if (window.parent !== window) return;
1945
+ } else if (target && target !== '_self') {
1946
+ return;
1947
+ }
1948
+
1949
+ const options = get_router_options(a);
1950
+ const is_svg_a_element = a instanceof SVGAElement;
1951
+
1952
+ // Ignore URL protocols that differ to the current one and are not http(s) (e.g. `mailto:`, `tel:`, `myapp:`, etc.)
1953
+ // This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external
1954
+ // navigation, but it's not clear how to handle that case and it's not likely to come up in practice.
1955
+ // MEMO: Without this condition, firefox will open mailer twice.
1956
+ // See:
1957
+ // - https://github.com/sveltejs/kit/issues/4045
1958
+ // - https://github.com/sveltejs/kit/issues/5725
1959
+ // - https://github.com/sveltejs/kit/issues/6496
1960
+ if (
1961
+ !is_svg_a_element &&
1962
+ url.protocol !== location.protocol &&
1963
+ !(url.protocol === 'https:' || url.protocol === 'http:')
1964
+ )
1965
+ return;
1966
+
1967
+ if (download) return;
1842
1968
 
1969
+ // Ignore the following but fire beforeNavigate
1970
+ if (external || options.reload) {
1971
+ if (_before_navigate({ url, type: 'link' })) {
1972
+ // set `navigating` to `true` to prevent `beforeNavigate` callbacks
1973
+ // being called when the page unloads
1974
+ navigating = true;
1975
+ } else {
1843
1976
  event.preventDefault();
1977
+ }
1844
1978
 
1845
- navigate({
1846
- type: 'link',
1847
- url,
1848
- keepfocus: options.keepfocus,
1849
- noscroll: options.noscroll,
1850
- replace_state: options.replace_state ?? url.href === location.href
1851
- });
1852
- });
1979
+ return;
1980
+ }
1853
1981
 
1854
- container.addEventListener('submit', (event) => {
1855
- if (event.defaultPrevented) return;
1982
+ // Check if new url only differs by hash and use the browser default behavior in that case
1983
+ // This will ensure the `hashchange` event is fired
1984
+ // Removing the hash does a full page navigation in the browser, so make sure a hash is present
1985
+ const [nonhash, hash] = url.href.split('#');
1986
+ if (hash !== undefined && nonhash === strip_hash(location)) {
1987
+ // If we are trying to navigate to the same hash, we should only
1988
+ // attempt to scroll to that element and avoid any history changes.
1989
+ // Otherwise, this can cause Firefox to incorrectly assign a null
1990
+ // history state value without any signal that we can detect.
1991
+ const [, current_hash] = current.url.href.split('#');
1992
+ if (current_hash === hash) {
1993
+ event.preventDefault();
1856
1994
 
1857
- const form = /** @type {HTMLFormElement} */ (
1858
- HTMLFormElement.prototype.cloneNode.call(event.target)
1859
- );
1995
+ // We're already on /# and click on a link that goes to /#, or we're on
1996
+ // /#top and click on a link that goes to /#top. In those cases just go to
1997
+ // the top of the page, and avoid a history change.
1998
+ if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) {
1999
+ window.scrollTo({ top: 0 });
2000
+ } else {
2001
+ a.ownerDocument.getElementById(hash)?.scrollIntoView();
2002
+ }
1860
2003
 
1861
- const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ (
1862
- event.submitter
1863
- );
2004
+ return;
2005
+ }
2006
+ // set this flag to distinguish between navigations triggered by
2007
+ // clicking a hash link and those triggered by popstate
2008
+ hash_navigating = true;
1864
2009
 
1865
- const method = submitter?.formMethod || form.method;
2010
+ update_scroll_positions(current_history_index);
1866
2011
 
1867
- if (method !== 'get') return;
2012
+ update_url(url);
1868
2013
 
1869
- const url = new URL(
1870
- (submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action
1871
- );
2014
+ if (!options.replace_state) return;
1872
2015
 
1873
- if (is_external_url(url, base)) return;
2016
+ // hashchange event shouldn't occur if the router is replacing state.
2017
+ hash_navigating = false;
2018
+ }
1874
2019
 
1875
- const event_form = /** @type {HTMLFormElement} */ (event.target);
2020
+ event.preventDefault();
1876
2021
 
1877
- const options = get_router_options(event_form);
1878
- if (options.reload) return;
2022
+ navigate({
2023
+ type: 'link',
2024
+ url,
2025
+ keepfocus: options.keepfocus,
2026
+ noscroll: options.noscroll,
2027
+ replace_state: options.replace_state ?? url.href === location.href
2028
+ });
2029
+ });
1879
2030
 
1880
- event.preventDefault();
1881
- event.stopPropagation();
2031
+ container.addEventListener('submit', (event) => {
2032
+ if (event.defaultPrevented) return;
1882
2033
 
1883
- const data = new FormData(event_form);
2034
+ const form = /** @type {HTMLFormElement} */ (
2035
+ HTMLFormElement.prototype.cloneNode.call(event.target)
2036
+ );
1884
2037
 
1885
- const submitter_name = submitter?.getAttribute('name');
1886
- if (submitter_name) {
1887
- data.append(submitter_name, submitter?.getAttribute('value') ?? '');
1888
- }
2038
+ const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ (event.submitter);
1889
2039
 
1890
- // @ts-expect-error `URLSearchParams(fd)` is kosher, but typescript doesn't know that
1891
- url.search = new URLSearchParams(data).toString();
2040
+ const method = submitter?.formMethod || form.method;
1892
2041
 
1893
- navigate({
1894
- type: 'form',
1895
- url,
1896
- keepfocus: options.keepfocus,
1897
- noscroll: options.noscroll,
1898
- replace_state: options.replace_state ?? url.href === location.href
1899
- });
1900
- });
2042
+ if (method !== 'get') return;
1901
2043
 
1902
- addEventListener('popstate', async (event) => {
1903
- if (event.state?.[HISTORY_INDEX]) {
1904
- const history_index = event.state[HISTORY_INDEX];
1905
- token = {};
1906
-
1907
- // if a popstate-driven navigation is cancelled, we need to counteract it
1908
- // with history.go, which means we end up back here, hence this check
1909
- if (history_index === current_history_index) return;
1910
-
1911
- const scroll = scroll_positions[history_index];
1912
- const state = states[history_index] ?? {};
1913
- const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
1914
- const navigation_index = event.state[NAVIGATION_INDEX];
1915
- const is_hash_change = strip_hash(location) === strip_hash(current.url);
1916
- const shallow =
1917
- navigation_index === current_navigation_index && (has_navigated || is_hash_change);
1918
-
1919
- if (shallow) {
1920
- // We don't need to navigate, we just need to update scroll and/or state.
1921
- // This happens with hash links and `pushState`/`replaceState`. The
1922
- // exception is if we haven't navigated yet, since we could have
1923
- // got here after a modal navigation then a reload
1924
- update_url(url);
1925
-
1926
- scroll_positions[current_history_index] = scroll_state();
1927
- if (scroll) scrollTo(scroll.x, scroll.y);
1928
-
1929
- if (state !== page.state) {
1930
- page = { ...page, state };
1931
- root.$set({ page });
1932
- }
2044
+ const url = new URL(
2045
+ (submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action
2046
+ );
1933
2047
 
1934
- current_history_index = history_index;
1935
- return;
1936
- }
2048
+ if (is_external_url(url, base)) return;
1937
2049
 
1938
- const delta = history_index - current_history_index;
2050
+ const event_form = /** @type {HTMLFormElement} */ (event.target);
1939
2051
 
1940
- await navigate({
1941
- type: 'popstate',
1942
- url,
1943
- popped: {
1944
- state,
1945
- scroll,
1946
- delta
1947
- },
1948
- accept: () => {
1949
- current_history_index = history_index;
1950
- current_navigation_index = navigation_index;
1951
- },
1952
- block: () => {
1953
- history.go(-delta);
1954
- },
1955
- nav_token: token
1956
- });
1957
- } else {
1958
- // since popstate event is also emitted when an anchor referencing the same
1959
- // document is clicked, we have to check that the router isn't already handling
1960
- // the navigation. otherwise we would be updating the page store twice.
1961
- if (!hash_navigating) {
1962
- const url = new URL(location.href);
1963
- update_url(url);
1964
- }
1965
- }
1966
- });
2052
+ const options = get_router_options(event_form);
2053
+ if (options.reload) return;
1967
2054
 
1968
- addEventListener('hashchange', () => {
1969
- // if the hashchange happened as a result of clicking on a link,
1970
- // we need to update history, otherwise we have to leave it alone
1971
- if (hash_navigating) {
1972
- hash_navigating = false;
1973
- original_replace_state.call(
1974
- history,
1975
- {
1976
- ...history.state,
1977
- [HISTORY_INDEX]: ++current_history_index,
1978
- [NAVIGATION_INDEX]: current_navigation_index
1979
- },
1980
- '',
1981
- location.href
1982
- );
1983
- }
1984
- });
2055
+ event.preventDefault();
2056
+ event.stopPropagation();
1985
2057
 
1986
- // fix link[rel=icon], because browsers will occasionally try to load relative
1987
- // URLs after a pushState/replaceState, resulting in a 404 — see
1988
- // https://github.com/sveltejs/kit/issues/3748#issuecomment-1125980897
1989
- for (const link of document.querySelectorAll('link')) {
1990
- if (link.rel === 'icon') link.href = link.href; // eslint-disable-line
1991
- }
2058
+ const data = new FormData(event_form);
2059
+
2060
+ const submitter_name = submitter?.getAttribute('name');
2061
+ if (submitter_name) {
2062
+ data.append(submitter_name, submitter?.getAttribute('value') ?? '');
2063
+ }
1992
2064
 
1993
- addEventListener('pageshow', (event) => {
1994
- // If the user navigates to another site and then uses the back button and
1995
- // bfcache hits, we need to set navigating to null, the site doesn't know
1996
- // the navigation away from it was successful.
1997
- // Info about bfcache here: https://web.dev/bfcache
1998
- if (event.persisted) {
1999
- stores.navigating.set(null);
2065
+ // @ts-expect-error `URLSearchParams(fd)` is kosher, but typescript doesn't know that
2066
+ url.search = new URLSearchParams(data).toString();
2067
+
2068
+ navigate({
2069
+ type: 'form',
2070
+ url,
2071
+ keepfocus: options.keepfocus,
2072
+ noscroll: options.noscroll,
2073
+ replace_state: options.replace_state ?? url.href === location.href
2074
+ });
2075
+ });
2076
+
2077
+ addEventListener('popstate', async (event) => {
2078
+ if (event.state?.[HISTORY_INDEX]) {
2079
+ const history_index = event.state[HISTORY_INDEX];
2080
+ token = {};
2081
+
2082
+ // if a popstate-driven navigation is cancelled, we need to counteract it
2083
+ // with history.go, which means we end up back here, hence this check
2084
+ if (history_index === current_history_index) return;
2085
+
2086
+ const scroll = scroll_positions[history_index];
2087
+ const state = event.state[STATES_KEY] ?? {};
2088
+ const url = new URL(event.state[PAGE_URL_KEY] ?? location.href);
2089
+ const navigation_index = event.state[NAVIGATION_INDEX];
2090
+ const is_hash_change = strip_hash(location) === strip_hash(current.url);
2091
+ const shallow =
2092
+ navigation_index === current_navigation_index && (has_navigated || is_hash_change);
2093
+
2094
+ if (shallow) {
2095
+ // We don't need to navigate, we just need to update scroll and/or state.
2096
+ // This happens with hash links and `pushState`/`replaceState`. The
2097
+ // exception is if we haven't navigated yet, since we could have
2098
+ // got here after a modal navigation then a reload
2099
+ update_url(url);
2100
+
2101
+ scroll_positions[current_history_index] = scroll_state();
2102
+ if (scroll) scrollTo(scroll.x, scroll.y);
2103
+
2104
+ if (state !== page.state) {
2105
+ page = { ...page, state };
2106
+ root.$set({ page });
2000
2107
  }
2001
- });
2002
2108
 
2003
- /**
2004
- * @param {URL} url
2005
- */
2006
- function update_url(url) {
2007
- current.url = url;
2008
- stores.page.set({ ...page, url });
2009
- stores.page.notify();
2109
+ current_history_index = history_index;
2110
+ return;
2010
2111
  }
2011
- },
2012
-
2013
- _hydrate: async ({
2014
- status = 200,
2015
- error,
2016
- node_ids,
2017
- params,
2018
- route,
2019
- data: server_data_nodes,
2020
- form
2021
- }) => {
2022
- hydrated = true;
2023
2112
 
2024
- const url = new URL(location.href);
2113
+ const delta = history_index - current_history_index;
2025
2114
 
2026
- if (!__SVELTEKIT_EMBEDDED__) {
2027
- // See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
2028
- // of determining the params on the client side.
2029
- ({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {});
2115
+ await navigate({
2116
+ type: 'popstate',
2117
+ url,
2118
+ popped: {
2119
+ state,
2120
+ scroll,
2121
+ delta
2122
+ },
2123
+ accept: () => {
2124
+ current_history_index = history_index;
2125
+ current_navigation_index = navigation_index;
2126
+ },
2127
+ block: () => {
2128
+ history.go(-delta);
2129
+ },
2130
+ nav_token: token
2131
+ });
2132
+ } else {
2133
+ // since popstate event is also emitted when an anchor referencing the same
2134
+ // document is clicked, we have to check that the router isn't already handling
2135
+ // the navigation. otherwise we would be updating the page store twice.
2136
+ if (!hash_navigating) {
2137
+ const url = new URL(location.href);
2138
+ update_url(url);
2030
2139
  }
2140
+ }
2141
+ });
2031
2142
 
2032
- /** @type {import('./types.js').NavigationFinished | undefined} */
2033
- let result;
2143
+ addEventListener('hashchange', () => {
2144
+ // if the hashchange happened as a result of clicking on a link,
2145
+ // we need to update history, otherwise we have to leave it alone
2146
+ if (hash_navigating) {
2147
+ hash_navigating = false;
2148
+ original_replace_state.call(
2149
+ history,
2150
+ {
2151
+ ...history.state,
2152
+ [HISTORY_INDEX]: ++current_history_index,
2153
+ [NAVIGATION_INDEX]: current_navigation_index
2154
+ },
2155
+ '',
2156
+ location.href
2157
+ );
2158
+ }
2159
+ });
2034
2160
 
2035
- try {
2036
- const branch_promises = node_ids.map(async (n, i) => {
2037
- const server_data_node = server_data_nodes[i];
2038
- // Type isn't completely accurate, we still need to deserialize uses
2039
- if (server_data_node?.uses) {
2040
- server_data_node.uses = deserialize_uses(server_data_node.uses);
2041
- }
2161
+ // fix link[rel=icon], because browsers will occasionally try to load relative
2162
+ // URLs after a pushState/replaceState, resulting in a 404 — see
2163
+ // https://github.com/sveltejs/kit/issues/3748#issuecomment-1125980897
2164
+ for (const link of document.querySelectorAll('link')) {
2165
+ if (link.rel === 'icon') link.href = link.href; // eslint-disable-line
2166
+ }
2042
2167
 
2043
- return load_node({
2044
- loader: app.nodes[n],
2045
- url,
2046
- params,
2047
- route,
2048
- parent: async () => {
2049
- const data = {};
2050
- for (let j = 0; j < i; j += 1) {
2051
- Object.assign(data, (await branch_promises[j]).data);
2052
- }
2053
- return data;
2054
- },
2055
- server_data_node: create_data_node(server_data_node)
2056
- });
2057
- });
2168
+ addEventListener('pageshow', (event) => {
2169
+ // If the user navigates to another site and then uses the back button and
2170
+ // bfcache hits, we need to set navigating to null, the site doesn't know
2171
+ // the navigation away from it was successful.
2172
+ // Info about bfcache here: https://web.dev/bfcache
2173
+ if (event.persisted) {
2174
+ stores.navigating.set(null);
2175
+ }
2176
+ });
2058
2177
 
2059
- /** @type {Array<import('./types.js').BranchNode | undefined>} */
2060
- const branch = await Promise.all(branch_promises);
2178
+ /**
2179
+ * @param {URL} url
2180
+ */
2181
+ function update_url(url) {
2182
+ current.url = url;
2183
+ stores.page.set({ ...page, url });
2184
+ stores.page.notify();
2185
+ }
2186
+ }
2061
2187
 
2062
- const parsed_route = routes.find(({ id }) => id === route.id);
2188
+ /**
2189
+ * @param {HTMLElement} target
2190
+ * @param {{
2191
+ * status: number;
2192
+ * error: App.Error | null;
2193
+ * node_ids: number[];
2194
+ * params: Record<string, string>;
2195
+ * route: { id: string | null };
2196
+ * data: Array<import('types').ServerDataNode | null>;
2197
+ * form: Record<string, any> | null;
2198
+ * }} opts
2199
+ */
2200
+ async function _hydrate(
2201
+ target,
2202
+ { status = 200, error, node_ids, params, route, data: server_data_nodes, form }
2203
+ ) {
2204
+ hydrated = true;
2205
+
2206
+ const url = new URL(location.href);
2207
+
2208
+ if (!__SVELTEKIT_EMBEDDED__) {
2209
+ // See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
2210
+ // of determining the params on the client side.
2211
+ ({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {});
2212
+ }
2063
2213
 
2064
- // server-side will have compacted the branch, reinstate empty slots
2065
- // so that error boundaries can be lined up correctly
2066
- if (parsed_route) {
2067
- const layouts = parsed_route.layouts;
2068
- for (let i = 0; i < layouts.length; i++) {
2069
- if (!layouts[i]) {
2070
- branch.splice(i, 0, undefined);
2071
- }
2214
+ /** @type {import('./types.js').NavigationFinished | undefined} */
2215
+ let result;
2216
+
2217
+ try {
2218
+ const branch_promises = node_ids.map(async (n, i) => {
2219
+ const server_data_node = server_data_nodes[i];
2220
+ // Type isn't completely accurate, we still need to deserialize uses
2221
+ if (server_data_node?.uses) {
2222
+ server_data_node.uses = deserialize_uses(server_data_node.uses);
2223
+ }
2224
+
2225
+ return load_node({
2226
+ loader: app.nodes[n],
2227
+ url,
2228
+ params,
2229
+ route,
2230
+ parent: async () => {
2231
+ const data = {};
2232
+ for (let j = 0; j < i; j += 1) {
2233
+ Object.assign(data, (await branch_promises[j]).data);
2072
2234
  }
2073
- }
2235
+ return data;
2236
+ },
2237
+ server_data_node: create_data_node(server_data_node)
2238
+ });
2239
+ });
2074
2240
 
2075
- result = await get_navigation_result_from_branch({
2076
- url,
2077
- params,
2078
- branch,
2079
- status,
2080
- error,
2081
- form,
2082
- route: parsed_route ?? null
2083
- });
2084
- } catch (error) {
2085
- if (error instanceof Redirect) {
2086
- // this is a real edge case — `load` would need to return
2087
- // a redirect but only in the browser
2088
- await native_navigation(new URL(error.location, location.href));
2089
- return;
2090
- }
2241
+ /** @type {Array<import('./types.js').BranchNode | undefined>} */
2242
+ const branch = await Promise.all(branch_promises);
2091
2243
 
2092
- result = await load_root_error_page({
2093
- status: get_status(error),
2094
- error: await handle_error(error, { url, params, route }),
2095
- url,
2096
- route
2097
- });
2098
- }
2244
+ const parsed_route = routes.find(({ id }) => id === route.id);
2099
2245
 
2100
- if (result.props.page) {
2101
- result.props.page.state = {};
2246
+ // server-side will have compacted the branch, reinstate empty slots
2247
+ // so that error boundaries can be lined up correctly
2248
+ if (parsed_route) {
2249
+ const layouts = parsed_route.layouts;
2250
+ for (let i = 0; i < layouts.length; i++) {
2251
+ if (!layouts[i]) {
2252
+ branch.splice(i, 0, undefined);
2253
+ }
2102
2254
  }
2255
+ }
2103
2256
 
2104
- initialize(result);
2257
+ result = await get_navigation_result_from_branch({
2258
+ url,
2259
+ params,
2260
+ branch,
2261
+ status,
2262
+ error,
2263
+ form,
2264
+ route: parsed_route ?? null
2265
+ });
2266
+ } catch (error) {
2267
+ if (error instanceof Redirect) {
2268
+ // this is a real edge case — `load` would need to return
2269
+ // a redirect but only in the browser
2270
+ await native_navigation(new URL(error.location, location.href));
2271
+ return;
2105
2272
  }
2106
- };
2273
+
2274
+ result = await load_root_error_page({
2275
+ status: get_status(error),
2276
+ error: await handle_error(error, { url, params, route }),
2277
+ url,
2278
+ route
2279
+ });
2280
+ }
2281
+
2282
+ if (result.props.page) {
2283
+ result.props.page.state = {};
2284
+ }
2285
+
2286
+ initialize(result, target);
2107
2287
  }
2108
2288
 
2109
2289
  /**