@sveltejs/kit 2.0.8 → 2.1.1

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