From aabe33c4f6097503c7dabf8a5b9a8a566981da2a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:57:31 +0100 Subject: [PATCH] feat: make client router treeshakeable (#11340) * feat: make client router treeshakeable This removes the `create_client` method in `client.js` in favor of having everything as exported functions at the top level. Slight rearrangements to make some things lazy or put it behind a `BROWSER ?` condition are necessary, but otherwise the code is almost completely untouched. This makes it even less likely we can ever unit test the client router, but I think that ship has sailed a long time ago and e2e tests are a much more robust and an outright better way to test this, so it's a non-issue. * changeset * fix PURE annotation positions * POC for potential solution: force deduplication of modules by appending a query string * refine embedded approach use an increasing id -> things are still cached, but dynamically, and Vite plays ball, too * hacking my way to victory * tweak * move whole startup logic into create_client * these can be private functions now * fix * Update packages/kit/src/runtime/client/start_embedded.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * chore: apply state to history.state directly (#11521) * chore: apply state to history.state directly The serialization capabilities of history.state are basically identical to those of devalue.stringify . As such, the indirection of saving the data in the session storage isn't necessary, which also saves 1kb compressed / 3kb uncompressed because of the no longer needed devalue functions * Apply suggestions from code review --------- Co-authored-by: Rich Harris * Reduce indirection in treeshakeable client (#11544) * reduce indirection * fix * tweak error messages * sigh --------- Co-authored-by: Rich Harris * remove all the embedded hackery (#11545) * remove all the embedded hackery * ugh --------- Co-authored-by: Rich Harris * reduce diff size --------- Co-authored-by: Rich Harris Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/mean-papayas-cross.md | 5 + .../docs/30-advanced/67-shallow-routing.md | 2 +- packages/kit/src/runtime/app/forms.js | 4 +- packages/kit/src/runtime/app/navigation.js | 155 +- packages/kit/src/runtime/client/client.js | 3395 +++++++++-------- packages/kit/src/runtime/client/fetcher.js | 9 +- packages/kit/src/runtime/client/singletons.js | 48 - packages/kit/src/runtime/client/start.js | 17 +- packages/kit/src/runtime/client/types.d.ts | 28 - packages/kit/types/index.d.ts | 93 +- 10 files changed, 1856 insertions(+), 1900 deletions(-) create mode 100644 .changeset/mean-papayas-cross.md diff --git a/.changeset/mean-papayas-cross.md b/.changeset/mean-papayas-cross.md new file mode 100644 index 000000000000..a65dc07400b4 --- /dev/null +++ b/.changeset/mean-papayas-cross.md @@ -0,0 +1,5 @@ +--- +"@sveltejs/kit": minor +--- + +feat: make client router treeshakeable diff --git a/documentation/docs/30-advanced/67-shallow-routing.md b/documentation/docs/30-advanced/67-shallow-routing.md index 2fea3e8510ea..61e49b58f844 100644 --- a/documentation/docs/30-advanced/67-shallow-routing.md +++ b/documentation/docs/30-advanced/67-shallow-routing.md @@ -93,6 +93,6 @@ For this to work, you need to load the data that the `+page.svelte` expects. A c ## Caveats -During server-side rendering, `$page.state` is always an empty object. The same is true for the first page the user lands on — if the user reloads the page, state will _not_ be applied until they navigate. +During server-side rendering, `$page.state` is always an empty object. The same is true for the first page the user lands on — if the user reloads the page (or returns from another document), state will _not_ be applied until they navigate. Shallow routing is a feature that requires JavaScript to work. Be mindful when using it and try to think of sensible fallback behavior in case JavaScript isn't available. diff --git a/packages/kit/src/runtime/app/forms.js b/packages/kit/src/runtime/app/forms.js index bd2a81fc8656..dd872efc6599 100644 --- a/packages/kit/src/runtime/app/forms.js +++ b/packages/kit/src/runtime/app/forms.js @@ -1,7 +1,7 @@ import * as devalue from 'devalue'; import { BROWSER, DEV } from 'esm-env'; -import { client } from '../client/singletons.js'; import { invalidateAll } from './navigation.js'; +import { apply_action } from '../client/client.js'; /** * This action updates the `form` property of the current page with the given data and updates `$page.status`. @@ -13,7 +13,7 @@ import { invalidateAll } from './navigation.js'; */ export function applyAction(result) { if (BROWSER) { - return client.apply_action(result); + return apply_action(result); } else { throw new Error('Cannot call applyAction(...) on the server'); } diff --git a/packages/kit/src/runtime/app/navigation.js b/packages/kit/src/runtime/app/navigation.js index 4e38b9966ee7..ca3f95dd5984 100644 --- a/packages/kit/src/runtime/app/navigation.js +++ b/packages/kit/src/runtime/app/navigation.js @@ -1,142 +1,13 @@ -import { client_method } from '../client/singletons.js'; - -/** - * 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. - * This is generally discouraged, since it breaks user expectations. - * @returns {void} - */ -export const disableScrollHandling = /* @__PURE__ */ client_method('disable_scroll_handling'); - -/** - * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. - * For external URLs, use `window.location = url` instead of calling `goto(url)`. - * - * @type {(url: string | URL, opts?: { replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; state?: App.PageState }) => Promise} - * @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. - * @param {Object} [opts] Options related to the navigation - * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` - * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation - * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body - * @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. - * @param {App.PageState} [opts.state] An optional object that will be available on the `$page.state` store - * @returns {Promise} - */ -export const goto = /* @__PURE__ */ client_method('goto'); - -/** - * 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. - * - * 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). - * To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL. - * - * The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned. - * This can be useful if you want to invalidate based on a pattern instead of a exact match. - * - * ```ts - * // Example: Match '/path' regardless of the query parameters - * import { invalidate } from '$app/navigation'; - * - * invalidate((url) => url.pathname === '/path'); - * ``` - * @type {(url: string | URL | ((url: URL) => boolean)) => Promise} - * @param {string | URL | ((url: URL) => boolean)} url The invalidated URL - * @returns {Promise} - */ -export const invalidate = /* @__PURE__ */ client_method('invalidate'); - -/** - * Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated. - * @type {() => Promise} - * @returns {Promise} - */ -export const invalidateAll = /* @__PURE__ */ client_method('invalidate_all'); - -/** - * Programmatically preloads the given page, which means - * 1. ensuring that the code for the page is loaded, and - * 2. calling the page's load function with the appropriate options. - * - * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `` element with `data-sveltekit-preload-data`. - * If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous. - * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete. - * - * @type {(href: string) => Promise>} - * @param {string} href Page to preload - * @returns {Promise<{ type: 'loaded'; status: number; data: Record } | { type: 'redirect'; location: string }>} - */ -export const preloadData = /* @__PURE__ */ client_method('preload_data'); - -/** - * Programmatically imports the code for routes that haven't yet been fetched. - * Typically, you might call this to speed up subsequent navigation. - * - * 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`). - * - * Unlike `preloadData`, this won't call `load` functions. - * Returns a Promise that resolves when the modules have been imported. - * - * @type {(url: string) => Promise} - * @param {string} url - * @returns {Promise} - */ -export const preloadCode = /* @__PURE__ */ client_method('preload_code'); - -/** - * 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. - * - * 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. - * - * 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`. - * - * 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`. - * - * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. - * @type {(callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void) => void} - * @param {(navigation: import('@sveltejs/kit').BeforeNavigate) => void} callback - * @returns {void} - */ -export const beforeNavigate = /* @__PURE__ */ client_method('before_navigate'); - -/** - * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. - * - * 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. - * - * 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. - * - * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. - * @type {(callback: (navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>) => void} - * @param {(navigation: import('@sveltejs/kit').OnNavigate) => void} callback - * @returns {void} - */ -export const onNavigate = /* @__PURE__ */ client_method('on_navigate'); - -/** - * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. - * - * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. - * @type {(callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void) => void} - * @param {(navigation: import('@sveltejs/kit').AfterNavigate) => void} callback - * @returns {void} - */ -export const afterNavigate = /* @__PURE__ */ client_method('after_navigate'); - -/** - * 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). - * - * @type {(url: string | URL, state: App.PageState) => void} - * @param {string | URL} url - * @param {App.PageState} state - * @returns {void} - */ -export const pushState = /* @__PURE__ */ client_method('push_state'); - -/** - * 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). - * - * @type {(url: string | URL, state: App.PageState) => void} - * @param {string | URL} url - * @param {App.PageState} state - * @returns {void} - */ -export const replaceState = /* @__PURE__ */ client_method('replace_state'); +export { + afterNavigate, + beforeNavigate, + disableScrollHandling, + goto, + invalidate, + invalidateAll, + onNavigate, + preloadCode, + preloadData, + pushState, + replaceState +} from '../client/client.js'; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index c3aa9c306c1d..7d485b1f980a 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -1,4 +1,4 @@ -import { DEV } from 'esm-env'; +import { BROWSER, DEV } from 'esm-env'; import { onMount, tick } from 'svelte'; import { add_data_suffix, @@ -26,7 +26,6 @@ import { origin, scroll_state } from './utils.js'; - import { base } from '__sveltekit/paths'; import * as devalue from 'devalue'; import { @@ -59,22 +58,16 @@ let errored = false; */ const scroll_positions = storage.get(SCROLL_KEY) ?? {}; -/** - * history index -> any - * @type {Record>} - */ -const states = storage.get(STATES_KEY, devalue.parse) ?? {}; - /** * navigation index -> any * @type {Record} */ const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; -const original_push_state = history.pushState; -const original_replace_state = history.replaceState; +const original_push_state = BROWSER ? history.pushState : () => {}; +const original_replace_state = BROWSER ? history.replaceState : () => {}; -if (DEV) { +if (DEV && BROWSER) { let warned = false; const warn = () => { @@ -135,78 +128,102 @@ function native_navigation(url) { function noop() {} +/** @type {import('types').CSRRoute[]} */ +let routes; +/** @type {import('types').CSRPageNodeLoader} */ +let default_layout_loader; +/** @type {import('types').CSRPageNodeLoader} */ +let default_error_loader; +/** @type {HTMLElement} */ +let container; +/** @type {HTMLElement} */ +let target; +/** @type {import('./types.js').SvelteKitApp} */ +let app; + +/** @type {Array<((url: URL) => boolean)>} */ +const invalidated = []; + /** - * @param {import('./types.js').SvelteKitApp} app - * @param {HTMLElement} target - * @returns {import('./types.js').Client} + * An array of the `+layout.svelte` and `+page.svelte` component instances + * that currently live on the page — used for capturing and restoring snapshots. + * It's updated/manipulated through `bind:this` in `Root.svelte`. + * @type {import('svelte').SvelteComponent[]} */ -export function create_client(app, target) { - const routes = parse(app); +const components = []; - const default_layout_loader = app.nodes[0]; - const default_error_loader = app.nodes[1]; +/** @type {{id: string, promise: Promise} | null} */ +let load_cache = null; - // we import the root layout/error nodes eagerly, so that - // connectivity errors after initialisation don't nuke the app - default_layout_loader(); - default_error_loader(); +const callbacks = { + /** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */ + before_navigate: [], - const container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement; + /** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */ + on_navigate: [], - /** @type {Array<((url: URL) => boolean)>} */ - const invalidated = []; + /** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */ + after_navigate: [] +}; - /** - * An array of the `+layout.svelte` and `+page.svelte` component instances - * that currently live on the page — used for capturing and restoring snapshots. - * It's updated/manipulated through `bind:this` in `Root.svelte`. - * @type {import('svelte').SvelteComponent[]} - */ - const components = []; +/** @type {import('./types.js').NavigationState} */ +let current = { + branch: [], + error: null, + // @ts-ignore - we need the initial value to be null + url: null +}; - /** @type {{id: string, promise: Promise} | null} */ - let load_cache = null; +/** this being true means we SSR'd */ +let hydrated = false; +let started = false; +let autoscroll = true; +let updating = false; +let navigating = false; +let hash_navigating = false; +/** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */ +let has_navigated = false; - const callbacks = { - /** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */ - before_navigate: [], +let force_invalidation = false; - /** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */ - on_navigate: [], +/** @type {import('svelte').SvelteComponent} */ +let root; - /** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */ - after_navigate: [] - }; +/** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */ +let current_history_index; - /** @type {import('./types.js').NavigationState} */ - let current = { - branch: [], - error: null, - // @ts-ignore - we need the initial value to be null - url: null - }; +/** @type {number} */ +let current_navigation_index; - /** this being true means we SSR'd */ - let hydrated = false; - let started = false; - let autoscroll = true; - let updating = false; - let navigating = false; - let hash_navigating = false; - /** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */ - let has_navigated = false; +/** @type {import('@sveltejs/kit').Page} */ +let page; - let force_invalidation = false; +/** @type {{}} */ +let token; - /** @type {import('svelte').SvelteComponent} */ - let root; +/** @type {Promise | null} */ +let pending_invalidate; - // keeping track of the history index in order to prevent popstate navigation events if needed - /** @type {number} */ - let current_history_index = history.state?.[HISTORY_INDEX]; +/** + * @param {import('./types.js').SvelteKitApp} _app + * @param {HTMLElement} _target + * @param {Parameters[1]} [hydrate] + */ +export async function create_client(_app, _target, hydrate) { + app = _app; + routes = parse(_app); + container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement; + target = _target; + + // we import the root layout/error nodes eagerly, so that + // connectivity errors after initialisation don't nuke the app + default_layout_loader = _app.nodes[0]; + default_error_loader = _app.nodes[1]; + default_layout_loader(); + default_error_loader(); - /** @type {number} */ - let current_navigation_index = history.state?.[NAVIGATION_INDEX]; + current_history_index = history.state?.[HISTORY_INDEX]; + current_navigation_index = history.state?.[NAVIGATION_INDEX]; if (!current_history_index) { // we use Date.now() as an offset so that cross-document navigations @@ -234,1876 +251,2018 @@ export function create_client(app, target) { scrollTo(scroll.x, scroll.y); } - /** @type {import('@sveltejs/kit').Page} */ - let page; - - /** @type {{}} */ - let token; + if (hydrate) { + await _hydrate(target, hydrate); + } else { + goto(location.href, { replaceState: true }); + } - /** @type {Promise | null} */ - let pending_invalidate; + _start_router(); +} - async function invalidate() { - // Accept all invalidations as they come, don't swallow any while another invalidation - // is running because subsequent invalidations may make earlier ones outdated, - // but batch multiple synchronous invalidations. - await (pending_invalidate ||= Promise.resolve()); - if (!pending_invalidate) return; - pending_invalidate = null; +async function _invalidate() { + // Accept all invalidations as they come, don't swallow any while another invalidation + // is running because subsequent invalidations may make earlier ones outdated, + // but batch multiple synchronous invalidations. + await (pending_invalidate ||= Promise.resolve()); + if (!pending_invalidate) return; + pending_invalidate = null; - const intent = get_navigation_intent(current.url, true); + const intent = get_navigation_intent(current.url, true); - // Clear preload, it might be affected by the invalidation. - // Also solves an edge case where a preload is triggered, the navigation for it - // was then triggered and is still running while the invalidation kicks in, - // at which point the invalidation should take over and "win". - load_cache = null; + // Clear preload, it might be affected by the invalidation. + // Also solves an edge case where a preload is triggered, the navigation for it + // was then triggered and is still running while the invalidation kicks in, + // at which point the invalidation should take over and "win". + load_cache = null; - const nav_token = (token = {}); - const navigation_result = intent && (await load_route(intent)); - if (nav_token !== token) return; + const nav_token = (token = {}); + const navigation_result = intent && (await load_route(intent)); + if (nav_token !== token) return; - if (navigation_result) { - if (navigation_result.type === 'redirect') { - await goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token); - } else { - if (navigation_result.props.page !== undefined) { - page = navigation_result.props.page; - } - root.$set(navigation_result.props); + if (navigation_result) { + if (navigation_result.type === 'redirect') { + await _goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token); + } else { + if (navigation_result.props.page !== undefined) { + page = navigation_result.props.page; } + root.$set(navigation_result.props); } - - invalidated.length = 0; } - /** @param {number} index */ - function capture_snapshot(index) { - if (components.some((c) => c?.snapshot)) { - snapshots[index] = components.map((c) => c?.snapshot?.capture()); - } - } + invalidated.length = 0; +} - /** @param {number} index */ - function restore_snapshot(index) { - snapshots[index]?.forEach((value, i) => { - components[i]?.snapshot?.restore(value); - }); +/** @param {number} index */ +function capture_snapshot(index) { + if (components.some((c) => c?.snapshot)) { + snapshots[index] = components.map((c) => c?.snapshot?.capture()); } +} + +/** @param {number} index */ +function restore_snapshot(index) { + snapshots[index]?.forEach((value, i) => { + components[i]?.snapshot?.restore(value); + }); +} - function persist_state() { - update_scroll_positions(current_history_index); - storage.set(SCROLL_KEY, scroll_positions); +function persist_state() { + update_scroll_positions(current_history_index); + storage.set(SCROLL_KEY, scroll_positions); - capture_snapshot(current_navigation_index); - storage.set(SNAPSHOT_KEY, snapshots); - storage.set(STATES_KEY, states, devalue.stringify); - } + capture_snapshot(current_navigation_index); + storage.set(SNAPSHOT_KEY, snapshots); +} - /** - * @param {string | URL} url - * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; state?: Record }} options - * @param {number} redirect_count - * @param {{}} [nav_token] - */ - async function goto(url, options, redirect_count, nav_token) { - return navigate({ - type: 'goto', - url: resolve_url(url), - keepfocus: options.keepFocus, - noscroll: options.noScroll, - replace_state: options.replaceState, - redirect_count, - state: options.state, - nav_token, - accept: () => { - if (options.invalidateAll) { - force_invalidation = true; - } +/** + * @param {string | URL} url + * @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; state?: Record }} options + * @param {number} redirect_count + * @param {{}} [nav_token] + */ +async function _goto(url, options, redirect_count, nav_token) { + return navigate({ + type: 'goto', + url: resolve_url(url), + keepfocus: options.keepFocus, + noscroll: options.noScroll, + replace_state: options.replaceState, + state: options.state, + redirect_count, + nav_token, + accept: () => { + if (options.invalidateAll) { + force_invalidation = true; } - }); - } + } + }); +} - /** @param {import('./types.js').NavigationIntent} intent */ - async function preload_data(intent) { - load_cache = { - id: intent.id, - promise: load_route(intent).then((result) => { - if (result.type === 'loaded' && result.state.error) { - // Don't cache errors, because they might be transient - load_cache = null; - } - return result; - }) - }; +/** @param {import('./types.js').NavigationIntent} intent */ +async function _preload_data(intent) { + load_cache = { + id: intent.id, + promise: load_route(intent).then((result) => { + if (result.type === 'loaded' && result.state.error) { + // Don't cache errors, because they might be transient + load_cache = null; + } + return result; + }) + }; - return load_cache.promise; - } + return load_cache.promise; +} - /** @param {string} pathname */ - async function preload_code(pathname) { - const route = routes.find((route) => route.exec(get_url_path(pathname))); +/** @param {string} pathname */ +async function _preload_code(pathname) { + const route = routes.find((route) => route.exec(get_url_path(pathname))); - if (route) { - await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]())); - } + if (route) { + await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]())); } +} - /** @param {import('./types.js').NavigationFinished} result */ - function initialize(result) { - if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return; +/** + * @param {import('./types.js').NavigationFinished} result + * @param {HTMLElement} target + */ +function initialize(result, target) { + if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return; - current = result.state; + current = result.state; - const style = document.querySelector('style[data-sveltekit]'); - if (style) style.remove(); + const style = document.querySelector('style[data-sveltekit]'); + if (style) style.remove(); - page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page); + page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page); - root = new app.root({ - target, - props: { ...result.props, stores, components }, - hydrate: true - }); + root = new app.root({ + target, + props: { ...result.props, stores, components }, + hydrate: true + }); - restore_snapshot(current_navigation_index); + restore_snapshot(current_navigation_index); - /** @type {import('@sveltejs/kit').AfterNavigate} */ - const navigation = { - from: null, - to: { - params: current.params, - route: { id: current.route?.id ?? null }, - url: new URL(location.href) - }, - willUnload: false, - type: 'enter', - complete: Promise.resolve() - }; - callbacks.after_navigate.forEach((fn) => fn(navigation)); + /** @type {import('@sveltejs/kit').AfterNavigate} */ + const navigation = { + from: null, + to: { + params: current.params, + route: { id: current.route?.id ?? null }, + url: new URL(location.href) + }, + willUnload: false, + type: 'enter', + complete: Promise.resolve() + }; + callbacks.after_navigate.forEach((fn) => fn(navigation)); - started = true; - } + started = true; +} - /** - * - * @param {{ - * url: URL; - * params: Record; - * branch: Array; - * status: number; - * error: App.Error | null; - * route: import('types').CSRRoute | null; - * form?: Record | null; - * }} opts - */ - async function get_navigation_result_from_branch({ - url, - params, - branch, - status, - error, - route, - form - }) { - /** @type {import('types').TrailingSlash} */ - let slash = 'never'; - for (const node of branch) { - if (node?.slash !== undefined) slash = node.slash; - } - url.pathname = normalize_path(url.pathname, slash); - // eslint-disable-next-line - url.search = url.search; // turn `/?` into `/` - - /** @type {import('./types.js').NavigationFinished} */ - const result = { - type: 'loaded', - state: { - url, - params, - branch, - error, - route - }, - props: { - // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up - constructors: compact(branch).map((branch_node) => branch_node.node.component), - page - } - }; +/** + * + * @param {{ + * url: URL; + * params: Record; + * branch: Array; + * status: number; + * error: App.Error | null; + * route: import('types').CSRRoute | null; + * form?: Record | null; + * }} opts + */ +async function get_navigation_result_from_branch({ + url, + params, + branch, + status, + error, + route, + form +}) { + /** @type {import('types').TrailingSlash} */ + let slash = 'never'; + for (const node of branch) { + if (node?.slash !== undefined) slash = node.slash; + } + url.pathname = normalize_path(url.pathname, slash); + // eslint-disable-next-line + url.search = url.search; // turn `/?` into `/` - if (form !== undefined) { - result.props.form = form; + /** @type {import('./types.js').NavigationFinished} */ + const result = { + type: 'loaded', + state: { + url, + params, + branch, + error, + route + }, + props: { + // @ts-ignore Somehow it's getting SvelteComponent and SvelteComponentDev mixed up + constructors: compact(branch).map((branch_node) => branch_node.node.component), + page } + }; - let data = {}; - let data_changed = !page; + if (form !== undefined) { + result.props.form = form; + } - let p = 0; + let data = {}; + let data_changed = !page; - for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) { - const node = branch[i]; - const prev = current.branch[i]; + let p = 0; - if (node?.data !== prev?.data) data_changed = true; - if (!node) continue; + for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) { + const node = branch[i]; + const prev = current.branch[i]; - data = { ...data, ...node.data }; + if (node?.data !== prev?.data) data_changed = true; + if (!node) continue; - // Only set props if the node actually updated. This prevents needless rerenders. - if (data_changed) { - result.props[`data_${p}`] = data; - } + data = { ...data, ...node.data }; - p += 1; + // Only set props if the node actually updated. This prevents needless rerenders. + if (data_changed) { + result.props[`data_${p}`] = data; } - const page_changed = - !current.url || - url.href !== current.url.href || - current.error !== error || - (form !== undefined && form !== page.form) || - data_changed; + p += 1; + } - if (page_changed) { - result.props.page = { - error, - params, - route: { - id: route?.id ?? null - }, - state: {}, - status, - url: new URL(url), - form: form ?? null, - // The whole page store is updated, but this way the object reference stays the same - data: data_changed ? data : page.data - }; - } + const page_changed = + !current.url || + url.href !== current.url.href || + current.error !== error || + (form !== undefined && form !== page.form) || + data_changed; - return result; + if (page_changed) { + result.props.page = { + error, + params, + route: { + id: route?.id ?? null + }, + state: {}, + status, + url: new URL(url), + form: form ?? null, + // The whole page store is updated, but this way the object reference stays the same + data: data_changed ? data : page.data + }; } - /** - * Call the load function of the given node, if it exists. - * If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested. - * - * @param {{ - * loader: import('types').CSRPageNodeLoader; - * parent: () => Promise>; - * url: URL; - * params: Record; - * route: { id: string | null }; - * server_data_node: import('./types.js').DataNode | null; - * }} options - * @returns {Promise} - */ - async function load_node({ loader, parent, url, params, route, server_data_node }) { - /** @type {Record | null} */ - let data = null; - - let is_tracking = true; - - /** @type {import('types').Uses} */ - const uses = { - dependencies: new Set(), - params: new Set(), - parent: false, - route: false, - url: false, - search_params: new Set() - }; + return result; +} - const node = await loader(); +/** + * Call the load function of the given node, if it exists. + * If `server_data` is passed, this is treated as the initial run and the page endpoint is not requested. + * + * @param {{ + * loader: import('types').CSRPageNodeLoader; + * parent: () => Promise>; + * url: URL; + * params: Record; + * route: { id: string | null }; + * server_data_node: import('./types.js').DataNode | null; + * }} options + * @returns {Promise} + */ +async function load_node({ loader, parent, url, params, route, server_data_node }) { + /** @type {Record | null} */ + let data = null; + + let is_tracking = true; + + /** @type {import('types').Uses} */ + const uses = { + dependencies: new Set(), + params: new Set(), + parent: false, + route: false, + url: false, + search_params: new Set() + }; - if (DEV) { - validate_page_exports(node.universal); - } + const node = await loader(); - if (node.universal?.load) { - /** @param {string[]} deps */ - function depends(...deps) { - for (const dep of deps) { - if (DEV) validate_depends(/** @type {string} */ (route.id), dep); + if (DEV) { + validate_page_exports(node.universal); + } - const { href } = new URL(dep, url); - uses.dependencies.add(href); - } - } + if (node.universal?.load) { + /** @param {string[]} deps */ + function depends(...deps) { + for (const dep of deps) { + if (DEV) validate_depends(/** @type {string} */ (route.id), dep); - /** @type {import('@sveltejs/kit').LoadEvent} */ - const load_input = { - route: new Proxy(route, { - get: (target, key) => { - if (is_tracking) { - uses.route = true; - } - return target[/** @type {'id'} */ (key)]; - } - }), - params: new Proxy(params, { - get: (target, key) => { - if (is_tracking) { - uses.params.add(/** @type {string} */ (key)); - } - return target[/** @type {string} */ (key)]; - } - }), - data: server_data_node?.data ?? null, - url: make_trackable( - url, - () => { - if (is_tracking) { - uses.url = true; - } - }, - (param) => { - if (is_tracking) { - uses.search_params.add(param); - } - } - ), - async fetch(resource, init) { - /** @type {URL | string} */ - let requested; - - if (resource instanceof Request) { - requested = resource.url; - - // we're not allowed to modify the received `Request` object, so in order - // to fixup relative urls we create a new equivalent `init` object instead - init = { - // the request body must be consumed in memory until browsers - // implement streaming request bodies and/or the body getter - body: - resource.method === 'GET' || resource.method === 'HEAD' - ? undefined - : await resource.blob(), - cache: resource.cache, - credentials: resource.credentials, - headers: resource.headers, - integrity: resource.integrity, - keepalive: resource.keepalive, - method: resource.method, - mode: resource.mode, - redirect: resource.redirect, - referrer: resource.referrer, - referrerPolicy: resource.referrerPolicy, - signal: resource.signal, - ...init - }; - } else { - requested = resource; - } + const { href } = new URL(dep, url); + uses.dependencies.add(href); + } + } - // we must fixup relative urls so they are resolved from the target page - const resolved = new URL(requested, url); + /** @type {import('@sveltejs/kit').LoadEvent} */ + const load_input = { + route: new Proxy(route, { + get: (target, key) => { if (is_tracking) { - depends(resolved.href); + uses.route = true; } - - // match ssr serialized data url, which is important to find cached responses - if (resolved.origin === url.origin) { - requested = resolved.href.slice(url.origin.length); + return target[/** @type {'id'} */ (key)]; + } + }), + params: new Proxy(params, { + get: (target, key) => { + if (is_tracking) { + uses.params.add(/** @type {string} */ (key)); } - - // prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved - return started - ? subsequent_fetch(requested, resolved.href, init) - : initial_fetch(requested, init); - }, - setHeaders: () => {}, // noop - depends, - parent() { + return target[/** @type {string} */ (key)]; + } + }), + data: server_data_node?.data ?? null, + url: make_trackable( + url, + () => { if (is_tracking) { - uses.parent = true; + uses.url = true; } - return parent(); }, - untrack(fn) { - is_tracking = false; - try { - return fn(); - } finally { - is_tracking = true; + (param) => { + if (is_tracking) { + uses.search_params.add(param); } } - }; + ), + async fetch(resource, init) { + /** @type {URL | string} */ + let requested; + + if (resource instanceof Request) { + requested = resource.url; + + // we're not allowed to modify the received `Request` object, so in order + // to fixup relative urls we create a new equivalent `init` object instead + init = { + // the request body must be consumed in memory until browsers + // implement streaming request bodies and/or the body getter + body: + resource.method === 'GET' || resource.method === 'HEAD' + ? undefined + : await resource.blob(), + cache: resource.cache, + credentials: resource.credentials, + headers: resource.headers, + integrity: resource.integrity, + keepalive: resource.keepalive, + method: resource.method, + mode: resource.mode, + redirect: resource.redirect, + referrer: resource.referrer, + referrerPolicy: resource.referrerPolicy, + signal: resource.signal, + ...init + }; + } else { + requested = resource; + } + + // we must fixup relative urls so they are resolved from the target page + const resolved = new URL(requested, url); + if (is_tracking) { + depends(resolved.href); + } + + // match ssr serialized data url, which is important to find cached responses + if (resolved.origin === url.origin) { + requested = resolved.href.slice(url.origin.length); + } - if (DEV) { + // prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved + return started + ? subsequent_fetch(requested, resolved.href, init) + : initial_fetch(requested, init); + }, + setHeaders: () => {}, // noop + depends, + parent() { + if (is_tracking) { + uses.parent = true; + } + return parent(); + }, + untrack(fn) { + is_tracking = false; try { - lock_fetch(); - data = (await node.universal.load.call(null, load_input)) ?? null; - if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { - throw new Error( - `a load function related to route '${route.id}' returned ${ - typeof data !== 'object' - ? `a ${typeof data}` - : data instanceof Response - ? 'a Response object' - : Array.isArray(data) - ? 'an array' - : 'a non-plain object' - }, but must return a plain object at the top level (i.e. \`return {...}\`)` - ); - } + return fn(); } finally { - unlock_fetch(); + is_tracking = true; } - } else { - data = (await node.universal.load.call(null, load_input)) ?? null; } - } - - return { - node, - loader, - server: server_data_node, - universal: node.universal?.load ? { type: 'data', data, uses } : null, - data: data ?? server_data_node?.data ?? null, - // if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of - // the `trailingSlash` route option, so that relative paths to JS and CSS work - slash: - base && (url.pathname === base || url.pathname === base + '/') - ? 'always' - : node.universal?.trailingSlash ?? server_data_node?.slash }; - } - /** - * @param {boolean} parent_changed - * @param {boolean} route_changed - * @param {boolean} url_changed - * @param {Set} search_params_changed - * @param {import('types').Uses | undefined} uses - * @param {Record} params - */ - function has_changed( - parent_changed, - route_changed, - url_changed, - search_params_changed, - uses, - params - ) { - if (force_invalidation) return true; - - if (!uses) return false; - - if (uses.parent && parent_changed) return true; - if (uses.route && route_changed) return true; - if (uses.url && url_changed) return true; - - for (const tracked_params of uses.search_params) { - if (search_params_changed.has(tracked_params)) return true; + if (DEV) { + try { + lock_fetch(); + data = (await node.universal.load.call(null, load_input)) ?? null; + if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { + throw new Error( + `a load function related to route '${route.id}' returned ${ + typeof data !== 'object' + ? `a ${typeof data}` + : data instanceof Response + ? 'a Response object' + : Array.isArray(data) + ? 'an array' + : 'a non-plain object' + }, but must return a plain object at the top level (i.e. \`return {...}\`)` + ); + } + } finally { + unlock_fetch(); + } + } else { + data = (await node.universal.load.call(null, load_input)) ?? null; } + } - for (const param of uses.params) { - if (params[param] !== current.params[param]) return true; - } + return { + node, + loader, + server: server_data_node, + universal: node.universal?.load ? { type: 'data', data, uses } : null, + data: data ?? server_data_node?.data ?? null, + // if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of + // the `trailingSlash` route option, so that relative paths to JS and CSS work + slash: + base && (url.pathname === base || url.pathname === base + '/') + ? 'always' + : node.universal?.trailingSlash ?? server_data_node?.slash + }; +} - for (const href of uses.dependencies) { - if (invalidated.some((fn) => fn(new URL(href)))) return true; - } +/** + * @param {boolean} parent_changed + * @param {boolean} route_changed + * @param {boolean} url_changed + * @param {Set} search_params_changed + * @param {import('types').Uses | undefined} uses + * @param {Record} params + */ +function has_changed( + parent_changed, + route_changed, + url_changed, + search_params_changed, + uses, + params +) { + if (force_invalidation) return true; + + if (!uses) return false; + + if (uses.parent && parent_changed) return true; + if (uses.route && route_changed) return true; + if (uses.url && url_changed) return true; + + for (const tracked_params of uses.search_params) { + if (search_params_changed.has(tracked_params)) return true; + } - return false; + for (const param of uses.params) { + if (params[param] !== current.params[param]) return true; } - /** - * @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node - * @param {import('./types.js').DataNode | null} [previous] - * @returns {import('./types.js').DataNode | null} - */ - function create_data_node(node, previous) { - if (node?.type === 'data') return node; - if (node?.type === 'skip') return previous ?? null; - return null; + for (const href of uses.dependencies) { + if (invalidated.some((fn) => fn(new URL(href)))) return true; } - /** - * - * @param {URL | null} old_url - * @param {URL} new_url - */ - function diff_search_params(old_url, new_url) { - if (!old_url) return new Set(new_url.searchParams.keys()); + return false; +} - const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]); +/** + * @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node + * @param {import('./types.js').DataNode | null} [previous] + * @returns {import('./types.js').DataNode | null} + */ +function create_data_node(node, previous) { + if (node?.type === 'data') return node; + if (node?.type === 'skip') return previous ?? null; + return null; +} - for (const key of changed) { - const old_values = old_url.searchParams.getAll(key); - const new_values = new_url.searchParams.getAll(key); +/** + * + * @param {URL | null} old_url + * @param {URL} new_url + */ +function diff_search_params(old_url, new_url) { + if (!old_url) return new Set(new_url.searchParams.keys()); - if ( - old_values.every((value) => new_values.includes(value)) && - new_values.every((value) => old_values.includes(value)) - ) { - changed.delete(key); - } - } + const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]); - return changed; - } + for (const key of changed) { + const old_values = old_url.searchParams.getAll(key); + const new_values = new_url.searchParams.getAll(key); - /** - * @param {import('./types.js').NavigationIntent} intent - * @returns {Promise} - */ - async function load_route({ id, invalidating, url, params, route }) { - if (load_cache?.id === id) { - return load_cache.promise; + if ( + old_values.every((value) => new_values.includes(value)) && + new_values.every((value) => old_values.includes(value)) + ) { + changed.delete(key); } + } - const { errors, layouts, leaf } = route; - - const loaders = [...layouts, leaf]; - - // preload modules to avoid waterfall, but handle rejections - // so they don't get reported to Sentry et al (we don't need - // to act on the failures at this point) - errors.forEach((loader) => loader?.().catch(() => {})); - loaders.forEach((loader) => loader?.[1]().catch(() => {})); - - /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */ - let server_data = null; - const url_changed = current.url ? id !== current.url.pathname + current.url.search : false; - const route_changed = current.route ? route.id !== current.route.id : false; - const search_params_changed = diff_search_params(current.url, url); - - let parent_invalid = false; - const invalid_server_nodes = loaders.map((loader, i) => { - const previous = current.branch[i]; - - const invalid = - !!loader?.[0] && - (previous?.loader !== loader[1] || - has_changed( - parent_invalid, - route_changed, - url_changed, - search_params_changed, - previous.server?.uses, - params - )); - - if (invalid) { - // For the next one - parent_invalid = true; - } - - return invalid; - }); - - if (invalid_server_nodes.some(Boolean)) { - try { - server_data = await load_data(url, invalid_server_nodes); - } catch (error) { - return load_root_error_page({ - status: get_status(error), - error: await handle_error(error, { url, params, route: { id: route.id } }), - url, - route - }); - } + return changed; +} - if (server_data.type === 'redirect') { - return server_data; - } - } +/** + * @param {import('./types.js').NavigationIntent} intent + * @returns {Promise} + */ +async function load_route({ id, invalidating, url, params, route }) { + if (load_cache?.id === id) { + return load_cache.promise; + } - const server_data_nodes = server_data?.nodes; + const { errors, layouts, leaf } = route; - let parent_changed = false; + const loaders = [...layouts, leaf]; - const branch_promises = loaders.map(async (loader, i) => { - if (!loader) return; + // preload modules to avoid waterfall, but handle rejections + // so they don't get reported to Sentry et al (we don't need + // to act on the failures at this point) + errors.forEach((loader) => loader?.().catch(() => {})); + loaders.forEach((loader) => loader?.[1]().catch(() => {})); - /** @type {import('./types.js').BranchNode | undefined} */ - const previous = current.branch[i]; + /** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */ + let server_data = null; + const url_changed = current.url ? id !== current.url.pathname + current.url.search : false; + const route_changed = current.route ? route.id !== current.route.id : false; + const search_params_changed = diff_search_params(current.url, url); - const server_data_node = server_data_nodes?.[i]; + let parent_invalid = false; + const invalid_server_nodes = loaders.map((loader, i) => { + const previous = current.branch[i]; - // re-use data from previous load if it's still valid - const valid = - (!server_data_node || server_data_node.type === 'skip') && - loader[1] === previous?.loader && - !has_changed( - parent_changed, + const invalid = + !!loader?.[0] && + (previous?.loader !== loader[1] || + has_changed( + parent_invalid, route_changed, url_changed, search_params_changed, - previous.universal?.uses, + previous.server?.uses, params - ); - if (valid) return previous; + )); - parent_changed = true; + if (invalid) { + // For the next one + parent_invalid = true; + } - if (server_data_node?.type === 'error') { - // rethrow and catch below - throw server_data_node; - } + return invalid; + }); - return load_node({ - loader: loader[1], + if (invalid_server_nodes.some(Boolean)) { + try { + server_data = await load_data(url, invalid_server_nodes); + } catch (error) { + return load_root_error_page({ + status: get_status(error), + error: await handle_error(error, { url, params, route: { id: route.id } }), url, - params, - route, - parent: async () => { - const data = {}; - for (let j = 0; j < i; j += 1) { - Object.assign(data, (await branch_promises[j])?.data); - } - return data; - }, - server_data_node: create_data_node( - // server_data_node is undefined if it wasn't reloaded from the server; - // and if current loader uses server data, we want to reuse previous data. - server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null, - loader[0] ? previous?.server : undefined - ) + route }); - }); + } - // if we don't do this, rejections will be unhandled - for (const p of branch_promises) p.catch(() => {}); + if (server_data.type === 'redirect') { + return server_data; + } + } - /** @type {Array} */ - const branch = []; + const server_data_nodes = server_data?.nodes; - for (let i = 0; i < loaders.length; i += 1) { - if (loaders[i]) { - try { - branch.push(await branch_promises[i]); - } catch (err) { - if (err instanceof Redirect) { - return { - type: 'redirect', - location: err.location - }; - } + let parent_changed = false; - let status = get_status(err); - /** @type {App.Error} */ - let error; - - if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) { - // this is the server error rethrown above, reconstruct but don't invoke - // the client error handler; it should've already been handled on the server - status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status; - error = /** @type {import('types').ServerErrorNode} */ (err).error; - } else if (err instanceof HttpError) { - error = err.body; - } else { - // Referenced node could have been removed due to redeploy, check - const updated = await stores.updated.check(); - if (updated) { - return await native_navigation(url); - } + const branch_promises = loaders.map(async (loader, i) => { + if (!loader) return; - error = await handle_error(err, { params, url, route: { id: route.id } }); - } + /** @type {import('./types.js').BranchNode | undefined} */ + const previous = current.branch[i]; - const error_load = await load_nearest_error_page(i, branch, errors); - if (error_load) { - return await get_navigation_result_from_branch({ - url, - params, - branch: branch.slice(0, error_load.idx).concat(error_load.node), - status, - error, - route - }); - } else { - // if we get here, it's because the root `load` function failed, - // and we need to fall back to the server - return await server_fallback(url, { id: route.id }, error, status); - } - } - } else { - // push an empty slot so we can rewind past gaps to the - // layout that corresponds with an +error.svelte page - branch.push(undefined); - } - } + const server_data_node = server_data_nodes?.[i]; - return await get_navigation_result_from_branch({ - url, + // re-use data from previous load if it's still valid + const valid = + (!server_data_node || server_data_node.type === 'skip') && + loader[1] === previous?.loader && + !has_changed( + parent_changed, + route_changed, + url_changed, + search_params_changed, + previous.universal?.uses, + params + ); + if (valid) return previous; + + parent_changed = true; + + if (server_data_node?.type === 'error') { + // rethrow and catch below + throw server_data_node; + } + + return load_node({ + loader: loader[1], + url, params, - branch, - status: 200, - error: null, route, - // Reset `form` on navigation, but not invalidation - form: invalidating ? undefined : null + parent: async () => { + const data = {}; + for (let j = 0; j < i; j += 1) { + Object.assign(data, (await branch_promises[j])?.data); + } + return data; + }, + server_data_node: create_data_node( + // server_data_node is undefined if it wasn't reloaded from the server; + // and if current loader uses server data, we want to reuse previous data. + server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null, + loader[0] ? previous?.server : undefined + ) }); - } + }); - /** - * @param {number} i Start index to backtrack from - * @param {Array} branch Branch to backtrack - * @param {Array} errors All error pages for this branch - * @returns {Promise<{idx: number; node: import('./types.js').BranchNode} | undefined>} - */ - async function load_nearest_error_page(i, branch, errors) { - while (i--) { - if (errors[i]) { - let j = i; - while (!branch[j]) j -= 1; - try { + // if we don't do this, rejections will be unhandled + for (const p of branch_promises) p.catch(() => {}); + + /** @type {Array} */ + const branch = []; + + for (let i = 0; i < loaders.length; i += 1) { + if (loaders[i]) { + try { + branch.push(await branch_promises[i]); + } catch (err) { + if (err instanceof Redirect) { return { - idx: j + 1, - node: { - node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(), - loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]), - data: {}, - server: null, - universal: null - } + type: 'redirect', + location: err.location }; - } catch (e) { - continue; + } + + let status = get_status(err); + /** @type {App.Error} */ + let error; + + if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) { + // this is the server error rethrown above, reconstruct but don't invoke + // the client error handler; it should've already been handled on the server + status = /** @type {import('types').ServerErrorNode} */ (err).status ?? status; + error = /** @type {import('types').ServerErrorNode} */ (err).error; + } else if (err instanceof HttpError) { + error = err.body; + } else { + // Referenced node could have been removed due to redeploy, check + const updated = await stores.updated.check(); + if (updated) { + return await native_navigation(url); + } + + error = await handle_error(err, { params, url, route: { id: route.id } }); + } + + const error_load = await load_nearest_error_page(i, branch, errors); + if (error_load) { + return await get_navigation_result_from_branch({ + url, + params, + branch: branch.slice(0, error_load.idx).concat(error_load.node), + status, + error, + route + }); + } else { + // if we get here, it's because the root `load` function failed, + // and we need to fall back to the server + return await server_fallback(url, { id: route.id }, error, status); } } + } else { + // push an empty slot so we can rewind past gaps to the + // layout that corresponds with an +error.svelte page + branch.push(undefined); } } - /** - * @param {{ - * status: number; - * error: App.Error; - * url: URL; - * route: { id: string | null } - * }} opts - * @returns {Promise} - */ - async function load_root_error_page({ status, error, url, route }) { - /** @type {Record} */ - const params = {}; // error page does not have params + return await get_navigation_result_from_branch({ + url, + params, + branch, + status: 200, + error: null, + route, + // Reset `form` on navigation, but not invalidation + form: invalidating ? undefined : null + }); +} + +/** + * @param {number} i Start index to backtrack from + * @param {Array} branch Branch to backtrack + * @param {Array} errors All error pages for this branch + * @returns {Promise<{idx: number; node: import('./types.js').BranchNode} | undefined>} + */ +async function load_nearest_error_page(i, branch, errors) { + while (i--) { + if (errors[i]) { + let j = i; + while (!branch[j]) j -= 1; + try { + return { + idx: j + 1, + node: { + node: await /** @type {import('types').CSRPageNodeLoader } */ (errors[i])(), + loader: /** @type {import('types').CSRPageNodeLoader } */ (errors[i]), + data: {}, + server: null, + universal: null + } + }; + } catch (e) { + continue; + } + } + } +} + +/** + * @param {{ + * status: number; + * error: App.Error; + * url: URL; + * route: { id: string | null } + * }} opts + * @returns {Promise} + */ +async function load_root_error_page({ status, error, url, route }) { + /** @type {Record} */ + const params = {}; // error page does not have params - /** @type {import('types').ServerDataNode | null} */ - let server_data_node = null; + /** @type {import('types').ServerDataNode | null} */ + let server_data_node = null; - const default_layout_has_server_load = app.server_loads[0] === 0; + const default_layout_has_server_load = app.server_loads[0] === 0; - if (default_layout_has_server_load) { - // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use - // existing root layout data - try { - const server_data = await load_data(url, [true]); + if (default_layout_has_server_load) { + // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use + // existing root layout data + try { + const server_data = await load_data(url, [true]); - if ( - server_data.type !== 'data' || - (server_data.nodes[0] && server_data.nodes[0].type !== 'data') - ) { - throw 0; - } + if ( + server_data.type !== 'data' || + (server_data.nodes[0] && server_data.nodes[0].type !== 'data') + ) { + throw 0; + } - server_data_node = server_data.nodes[0] ?? null; - } catch { - // at this point we have no choice but to fall back to the server, if it wouldn't - // bring us right back here, turning this into an endless loop - if (url.origin !== origin || url.pathname !== location.pathname || hydrated) { - await native_navigation(url); - } + server_data_node = server_data.nodes[0] ?? null; + } catch { + // at this point we have no choice but to fall back to the server, if it wouldn't + // bring us right back here, turning this into an endless loop + if (url.origin !== origin || url.pathname !== location.pathname || hydrated) { + await native_navigation(url); } } + } - const root_layout = await load_node({ - loader: default_layout_loader, - url, - params, - route, - parent: () => Promise.resolve({}), - server_data_node: create_data_node(server_data_node) - }); + const root_layout = await load_node({ + loader: default_layout_loader, + url, + params, + route, + parent: () => Promise.resolve({}), + server_data_node: create_data_node(server_data_node) + }); - /** @type {import('./types.js').BranchNode} */ - const root_error = { - node: await default_error_loader(), - loader: default_error_loader, - universal: null, - server: null, - data: null - }; + /** @type {import('./types.js').BranchNode} */ + const root_error = { + node: await default_error_loader(), + loader: default_error_loader, + universal: null, + server: null, + data: null + }; - return await get_navigation_result_from_branch({ - url, - params, - branch: [root_layout, root_error], - status, - error, - route: null - }); - } + return await get_navigation_result_from_branch({ + url, + params, + branch: [root_layout, root_error], + status, + error, + route: null + }); +} - /** - * @param {URL} url - * @param {boolean} invalidating - */ - function get_navigation_intent(url, invalidating) { - if (is_external_url(url, base)) return; +/** + * @param {URL} url + * @param {boolean} invalidating + */ +function get_navigation_intent(url, invalidating) { + if (is_external_url(url, base)) return; - const path = get_url_path(url.pathname); + const path = get_url_path(url.pathname); - for (const route of routes) { - const params = route.exec(path); + for (const route of routes) { + const params = route.exec(path); - if (params) { - const id = url.pathname + url.search; - /** @type {import('./types.js').NavigationIntent} */ - const intent = { id, invalidating, route, params: decode_params(params), url }; - return intent; - } + if (params) { + const id = url.pathname + url.search; + /** @type {import('./types.js').NavigationIntent} */ + const intent = { id, invalidating, route, params: decode_params(params), url }; + return intent; } } +} - /** @param {string} pathname */ - function get_url_path(pathname) { - return decode_pathname(pathname.slice(base.length) || '/'); - } +/** @param {string} pathname */ +function get_url_path(pathname) { + return decode_pathname(pathname.slice(base.length) || '/'); +} - /** - * @param {{ - * url: URL; - * type: import('@sveltejs/kit').Navigation["type"]; - * intent?: import('./types.js').NavigationIntent; - * delta?: number; - * }} opts - */ - function before_navigate({ url, type, intent, delta }) { - let should_block = false; +/** + * @param {{ + * url: URL; + * type: import('@sveltejs/kit').Navigation["type"]; + * intent?: import('./types.js').NavigationIntent; + * delta?: number; + * }} opts + */ +function _before_navigate({ url, type, intent, delta }) { + let should_block = false; + + const nav = create_navigation(current, intent, url, type); - const nav = create_navigation(current, intent, url, type); + if (delta !== undefined) { + nav.navigation.delta = delta; + } - if (delta !== undefined) { - nav.navigation.delta = delta; + const cancellable = { + ...nav.navigation, + cancel: () => { + should_block = true; + nav.reject(new Error('navigation was cancelled')); } + }; - const cancellable = { - ...nav.navigation, - cancel: () => { - should_block = true; - nav.reject(new Error('navigation was cancelled')); - } - }; + if (!navigating) { + // Don't run the event during redirects + callbacks.before_navigate.forEach((fn) => fn(cancellable)); + } - if (!navigating) { - // Don't run the event during redirects - callbacks.before_navigate.forEach((fn) => fn(cancellable)); - } + return should_block ? null : nav; +} - return should_block ? null : nav; +/** + * @param {{ + * type: import('@sveltejs/kit').Navigation["type"]; + * url: URL; + * popped?: { + * state: Record; + * scroll: { x: number, y: number }; + * delta: number; + * }; + * keepfocus?: boolean; + * noscroll?: boolean; + * replace_state?: boolean; + * state?: Record; + * redirect_count?: number; + * nav_token?: {}; + * accept?: () => void; + * block?: () => void; + * }} opts + */ +async function navigate({ + type, + url, + popped, + keepfocus, + noscroll, + replace_state, + state = {}, + redirect_count = 0, + nav_token = {}, + accept = noop, + block = noop +}) { + const intent = get_navigation_intent(url, false); + const nav = _before_navigate({ url, type, delta: popped?.delta, intent }); + + if (!nav) { + block(); + return; } - /** - * @param {{ - * type: import('@sveltejs/kit').Navigation["type"]; - * url: URL; - * popped?: { - * state: Record; - * scroll: { x: number, y: number }; - * delta: number; - * }; - * keepfocus?: boolean; - * noscroll?: boolean; - * replace_state?: boolean; - * state?: Record; - * redirect_count?: number; - * nav_token?: {}; - * accept?: () => void; - * block?: () => void; - * }} opts - */ - async function navigate({ - type, - url, - popped, - keepfocus, - noscroll, - replace_state, - state = {}, - redirect_count = 0, - nav_token = {}, - accept = noop, - block = noop - }) { - const intent = get_navigation_intent(url, false); - const nav = before_navigate({ url, type, delta: popped?.delta, intent }); - - if (!nav) { - block(); - return; - } + // store this before calling `accept()`, which may change the index + const previous_history_index = current_history_index; + const previous_navigation_index = current_navigation_index; - // store this before calling `accept()`, which may change the index - const previous_history_index = current_history_index; - const previous_navigation_index = current_navigation_index; + accept(); - accept(); + navigating = true; - navigating = true; + if (started) { + stores.navigating.set(nav.navigation); + } + + token = nav_token; + let navigation_result = intent && (await load_route(intent)); - if (started) { - stores.navigating.set(nav.navigation); + if (!navigation_result) { + if (is_external_url(url, base)) { + return await native_navigation(url); } + navigation_result = await server_fallback( + url, + { id: null }, + await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), { + url, + params: {}, + route: { id: null } + }), + 404 + ); + } - token = nav_token; - let navigation_result = intent && (await load_route(intent)); + // if this is an internal navigation intent, use the normalized + // URL for the rest of the function + url = intent?.url || url; - if (!navigation_result) { - if (is_external_url(url, base)) { - return await native_navigation(url); - } - navigation_result = await server_fallback( - url, - { id: null }, - await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), { + // abort if user navigated during update + if (token !== nav_token) { + nav.reject(new Error('navigation was aborted')); + return false; + } + + if (navigation_result.type === 'redirect') { + // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects + if (redirect_count >= 20) { + navigation_result = await load_root_error_page({ + status: 500, + error: await handle_error(new Error('Redirect loop'), { url, params: {}, route: { id: null } }), - 404 - ); - } - - // if this is an internal navigation intent, use the normalized - // URL for the rest of the function - url = intent?.url || url; - - // abort if user navigated during update - if (token !== nav_token) { - nav.reject(new Error('navigation was aborted')); + url, + route: { id: null } + }); + } else { + _goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token); return false; } - - if (navigation_result.type === 'redirect') { - // whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects - if (redirect_count >= 20) { - navigation_result = await load_root_error_page({ - status: 500, - error: await handle_error(new Error('Redirect loop'), { - url, - params: {}, - route: { id: null } - }), - url, - route: { id: null } - }); - } else { - goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token); - return false; - } - } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) { - const updated = await stores.updated.check(); - if (updated) { - await native_navigation(url); - } + } else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) { + const updated = await stores.updated.check(); + if (updated) { + await native_navigation(url); } + } - // reset invalidation only after a finished navigation. If there are redirects or - // additional invalidations, they should get the same invalidation treatment - invalidated.length = 0; - force_invalidation = false; + // reset invalidation only after a finished navigation. If there are redirects or + // additional invalidations, they should get the same invalidation treatment + invalidated.length = 0; + force_invalidation = false; - updating = true; + updating = true; - update_scroll_positions(previous_history_index); - capture_snapshot(previous_navigation_index); + update_scroll_positions(previous_history_index); + capture_snapshot(previous_navigation_index); - // ensure the url pathname matches the page's trailing slash option - if (navigation_result.props.page.url.pathname !== url.pathname) { - url.pathname = navigation_result.props.page.url.pathname; - } + // ensure the url pathname matches the page's trailing slash option + if (navigation_result.props.page.url.pathname !== url.pathname) { + url.pathname = navigation_result.props.page.url.pathname; + } - state = popped ? popped.state : state; + state = popped ? popped.state : state; - if (!popped) { - // this is a new navigation, rather than a popstate - const change = replace_state ? 0 : 1; + if (!popped) { + // this is a new navigation, rather than a popstate + const change = replace_state ? 0 : 1; - const entry = { - [HISTORY_INDEX]: (current_history_index += change), - [NAVIGATION_INDEX]: (current_navigation_index += change) - }; + const entry = { + [HISTORY_INDEX]: (current_history_index += change), + [NAVIGATION_INDEX]: (current_navigation_index += change), + [STATES_KEY]: state + }; - const fn = replace_state ? original_replace_state : original_push_state; - fn.call(history, entry, '', url); + const fn = replace_state ? original_replace_state : original_push_state; + fn.call(history, entry, '', url); - if (!replace_state) { - clear_onward_history(current_history_index, current_navigation_index); - } + if (!replace_state) { + clear_onward_history(current_history_index, current_navigation_index); } + } - states[current_history_index] = state; - - // reset preload synchronously after the history state has been set to avoid race conditions - load_cache = null; + // reset preload synchronously after the history state has been set to avoid race conditions + load_cache = null; - navigation_result.props.page.state = state; + navigation_result.props.page.state = state; - if (started) { - current = navigation_result.state; + if (started) { + current = navigation_result.state; - // reset url before updating page store - if (navigation_result.props.page) { - navigation_result.props.page.url = url; - } + // reset url before updating page store + if (navigation_result.props.page) { + navigation_result.props.page.url = url; + } - const after_navigate = ( - await Promise.all( - callbacks.on_navigate.map((fn) => - fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation)) - ) + const after_navigate = ( + await Promise.all( + callbacks.on_navigate.map((fn) => + fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation)) ) - ).filter((value) => typeof value === 'function'); - - if (after_navigate.length > 0) { - function cleanup() { - callbacks.after_navigate = callbacks.after_navigate.filter( - // @ts-ignore - (fn) => !after_navigate.includes(fn) - ); - } - - after_navigate.push(cleanup); + ) + ).filter((value) => typeof value === 'function'); - // @ts-ignore - callbacks.after_navigate.push(...after_navigate); + if (after_navigate.length > 0) { + function cleanup() { + callbacks.after_navigate = callbacks.after_navigate.filter( + // @ts-ignore + (fn) => !after_navigate.includes(fn) + ); } - root.$set(navigation_result.props); - has_navigated = true; - } else { - initialize(navigation_result); + after_navigate.push(cleanup); + + // @ts-ignore + callbacks.after_navigate.push(...after_navigate); } - const { activeElement } = document; + root.$set(navigation_result.props); + has_navigated = true; + } else { + initialize(navigation_result, target); + } - // need to render the DOM before we can scroll to the rendered elements and do focus management - await tick(); + const { activeElement } = document; - // we reset scroll before dealing with focus, to avoid a flash of unscrolled content - const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null; - - if (autoscroll) { - const deep_linked = - url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))); - if (scroll) { - scrollTo(scroll.x, scroll.y); - } else if (deep_linked) { - // Here we use `scrollIntoView` on the element instead of `scrollTo` - // because it natively supports the `scroll-margin` and `scroll-behavior` - // CSS properties. - deep_linked.scrollIntoView(); - } else { - scrollTo(0, 0); - } - } + // need to render the DOM before we can scroll to the rendered elements and do focus management + await tick(); - const changed_focus = - // reset focus only if any manual focus management didn't override it - document.activeElement !== activeElement && - // also refocus when activeElement is body already because the - // focus event might not have been fired on it yet - document.activeElement !== document.body; + // we reset scroll before dealing with focus, to avoid a flash of unscrolled content + const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null; - if (!keepfocus && !changed_focus) { - reset_focus(); + if (autoscroll) { + const deep_linked = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))); + if (scroll) { + scrollTo(scroll.x, scroll.y); + } else if (deep_linked) { + // Here we use `scrollIntoView` on the element instead of `scrollTo` + // because it natively supports the `scroll-margin` and `scroll-behavior` + // CSS properties. + deep_linked.scrollIntoView(); + } else { + scrollTo(0, 0); } + } - autoscroll = true; - - if (navigation_result.props.page) { - page = navigation_result.props.page; - } + const changed_focus = + // reset focus only if any manual focus management didn't override it + document.activeElement !== activeElement && + // also refocus when activeElement is body already because the + // focus event might not have been fired on it yet + document.activeElement !== document.body; - navigating = false; + if (!keepfocus && !changed_focus) { + reset_focus(); + } - if (type === 'popstate') { - restore_snapshot(current_navigation_index); - } + autoscroll = true; - nav.fulfil(undefined); + if (navigation_result.props.page) { + page = navigation_result.props.page; + } - callbacks.after_navigate.forEach((fn) => - fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation)) - ); - stores.navigating.set(null); + navigating = false; - updating = false; + if (type === 'popstate') { + restore_snapshot(current_navigation_index); } - /** - * Does a full page reload if it wouldn't result in an endless loop in the SPA case - * @param {URL} url - * @param {{ id: string | null }} route - * @param {App.Error} error - * @param {number} status - * @returns {Promise} - */ - async function server_fallback(url, route, error, status) { - if (url.origin === origin && url.pathname === location.pathname && !hydrated) { - // We would reload the same page we're currently on, which isn't hydrated, - // which means no SSR, which means we would end up in an endless loop - return await load_root_error_page({ - status, - error, - url, - route - }); - } - - if (DEV && status !== 404) { - console.error( - 'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)' - ); + nav.fulfil(undefined); - debugger; // eslint-disable-line - } + callbacks.after_navigate.forEach((fn) => + fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation)) + ); + stores.navigating.set(null); - return await native_navigation(url); - } + updating = false; +} - if (import.meta.hot) { - import.meta.hot.on('vite:beforeUpdate', () => { - if (current.error) location.reload(); +/** + * Does a full page reload if it wouldn't result in an endless loop in the SPA case + * @param {URL} url + * @param {{ id: string | null }} route + * @param {App.Error} error + * @param {number} status + * @returns {Promise} + */ +async function server_fallback(url, route, error, status) { + if (url.origin === origin && url.pathname === location.pathname && !hydrated) { + // We would reload the same page we're currently on, which isn't hydrated, + // which means no SSR, which means we would end up in an endless loop + return await load_root_error_page({ + status, + error, + url, + route }); } - function setup_preload() { - /** @type {NodeJS.Timeout} */ - let mousemove_timeout; + if (DEV && status !== 404) { + console.error( + 'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)' + ); - container.addEventListener('mousemove', (event) => { - const target = /** @type {Element} */ (event.target); + debugger; // eslint-disable-line + } - clearTimeout(mousemove_timeout); - mousemove_timeout = setTimeout(() => { - preload(target, 2); - }, 20); - }); + return await native_navigation(url); +} - /** @param {Event} event */ - function tap(event) { - preload(/** @type {Element} */ (event.composedPath()[0]), 1); - } +if (import.meta.hot) { + import.meta.hot.on('vite:beforeUpdate', () => { + if (current.error) location.reload(); + }); +} - container.addEventListener('mousedown', tap); - container.addEventListener('touchstart', tap, { passive: true }); +function setup_preload() { + /** @type {NodeJS.Timeout} */ + let mousemove_timeout; - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - if (entry.isIntersecting) { - preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href); - observer.unobserve(entry.target); - } - } - }, - { threshold: 0 } - ); + container.addEventListener('mousemove', (event) => { + const target = /** @type {Element} */ (event.target); - /** - * @param {Element} element - * @param {number} priority - */ - function preload(element, priority) { - const a = find_anchor(element, container); - if (!a) return; + clearTimeout(mousemove_timeout); + mousemove_timeout = setTimeout(() => { + preload(target, 2); + }, 20); + }); - const { url, external, download } = get_link_info(a, base); - if (external || download) return; + /** @param {Event} event */ + function tap(event) { + preload(/** @type {Element} */ (event.composedPath()[0]), 1); + } - const options = get_router_options(a); + container.addEventListener('mousedown', tap); + container.addEventListener('touchstart', tap, { passive: true }); - if (!options.reload) { - if (priority <= options.preload_data) { - const intent = get_navigation_intent(/** @type {URL} */ (url), false); - if (intent) { - if (DEV) { - preload_data(intent).then((result) => { - if (result.type === 'loaded' && result.state.error) { - console.warn( - `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` + - 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' + - 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' + - 'See https://kit.svelte.dev/docs/link-options for more info' - ); - } - }); - } else { - preload_data(intent); - } + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + _preload_code(/** @type {HTMLAnchorElement} */ (entry.target).href); + observer.unobserve(entry.target); + } + } + }, + { threshold: 0 } + ); + + /** + * @param {Element} element + * @param {number} priority + */ + function preload(element, priority) { + const a = find_anchor(element, container); + if (!a) return; + + const { url, external, download } = get_link_info(a, base); + if (external || download) return; + + const options = get_router_options(a); + + if (!options.reload) { + if (priority <= options.preload_data) { + const intent = get_navigation_intent(/** @type {URL} */ (url), false); + if (intent) { + if (DEV) { + _preload_data(intent).then((result) => { + if (result.type === 'loaded' && result.state.error) { + console.warn( + `Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` + + 'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' + + 'This route was preloaded due to a data-sveltekit-preload-data attribute. ' + + 'See https://kit.svelte.dev/docs/link-options for more info' + ); + } + }); + } else { + _preload_data(intent); } - } else if (priority <= options.preload_code) { - preload_code(/** @type {URL} */ (url).pathname); } + } else if (priority <= options.preload_code) { + _preload_code(/** @type {URL} */ (url).pathname); } } + } - function after_navigate() { - observer.disconnect(); + function after_navigate() { + observer.disconnect(); - for (const a of container.querySelectorAll('a')) { - const { url, external, download } = get_link_info(a, base); - if (external || download) continue; + for (const a of container.querySelectorAll('a')) { + const { url, external, download } = get_link_info(a, base); + if (external || download) continue; - const options = get_router_options(a); - if (options.reload) continue; + const options = get_router_options(a); + if (options.reload) continue; - if (options.preload_code === PRELOAD_PRIORITIES.viewport) { - observer.observe(a); - } + if (options.preload_code === PRELOAD_PRIORITIES.viewport) { + observer.observe(a); + } - if (options.preload_code === PRELOAD_PRIORITIES.eager) { - preload_code(/** @type {URL} */ (url).pathname); - } + if (options.preload_code === PRELOAD_PRIORITIES.eager) { + _preload_code(/** @type {URL} */ (url).pathname); } } - - callbacks.after_navigate.push(after_navigate); - after_navigate(); } - /** - * @param {unknown} error - * @param {import('@sveltejs/kit').NavigationEvent} event - * @returns {import('types').MaybePromise} - */ - function handle_error(error, event) { - if (error instanceof HttpError) { - return error.body; - } - - if (DEV) { - errored = true; - console.warn('The next HMR update will cause the page to reload'); - } + callbacks.after_navigate.push(after_navigate); + after_navigate(); +} - const status = get_status(error); - const message = get_message(error); +/** + * @param {unknown} error + * @param {import('@sveltejs/kit').NavigationEvent} event + * @returns {import('types').MaybePromise} + */ +function handle_error(error, event) { + if (error instanceof HttpError) { + return error.body; + } - return ( - app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message }) - ); + if (DEV) { + errored = true; + console.warn('The next HMR update will cause the page to reload'); } - return { - after_navigate: (fn) => { - onMount(() => { - callbacks.after_navigate.push(fn); + const status = get_status(error); + const message = get_message(error); - return () => { - const i = callbacks.after_navigate.indexOf(fn); - callbacks.after_navigate.splice(i, 1); - }; - }); - }, + return ( + app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message }) + ); +} - before_navigate: (fn) => { - onMount(() => { - callbacks.before_navigate.push(fn); +/** + * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. + * + * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * @param {(navigation: import('@sveltejs/kit').AfterNavigate) => void} callback + * @returns {void} + */ +export function afterNavigate(callback) { + onMount(() => { + callbacks.after_navigate.push(callback); - return () => { - const i = callbacks.before_navigate.indexOf(fn); - callbacks.before_navigate.splice(i, 1); - }; - }); - }, + return () => { + const i = callbacks.after_navigate.indexOf(callback); + callbacks.after_navigate.splice(i, 1); + }; + }); +} - on_navigate: (fn) => { - onMount(() => { - callbacks.on_navigate.push(fn); +/** + * 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. + * + * 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. + * + * 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`. + * + * 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`. + * + * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * @param {(navigation: import('@sveltejs/kit').BeforeNavigate) => void} callback + * @returns {void} + */ +export function beforeNavigate(callback) { + onMount(() => { + callbacks.before_navigate.push(callback); - return () => { - const i = callbacks.on_navigate.indexOf(fn); - callbacks.on_navigate.splice(i, 1); - }; - }); - }, + return () => { + const i = callbacks.before_navigate.indexOf(callback); + callbacks.before_navigate.splice(i, 1); + }; + }); +} - disable_scroll_handling: () => { - if (DEV && started && !updating) { - throw new Error('Can only disable scroll handling during navigation'); - } +/** + * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. + * + * 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. + * + * 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. + * + * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * @param {(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise} callback + * @returns {void} + */ +export function onNavigate(callback) { + onMount(() => { + callbacks.on_navigate.push(callback); - if (updating || !started) { - autoscroll = false; - } - }, + return () => { + const i = callbacks.on_navigate.indexOf(callback); + callbacks.on_navigate.splice(i, 1); + }; + }); +} - goto: (url, opts = {}) => { - url = resolve_url(url); +/** + * 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. + * This is generally discouraged, since it breaks user expectations. + * @returns {void} + */ +export function disableScrollHandling() { + if (!BROWSER) { + throw new Error('Cannot call disableScrollHandling() on the server'); + } - if (url.origin !== origin) { - return Promise.reject( - new Error( - DEV - ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead` - : 'goto: invalid URL' - ) - ); - } + if (DEV && started && !updating) { + throw new Error('Can only disable scroll handling during navigation'); + } - return goto(url, opts, 0); - }, + if (updating || !started) { + autoscroll = false; + } +} - invalidate: (resource) => { - if (typeof resource === 'function') { - invalidated.push(resource); - } else { - const { href } = new URL(resource, location.href); - invalidated.push((url) => url.href === href); - } +/** + * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. + * For external URLs, use `window.location = url` instead of calling `goto(url)`. + * + * @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. + * @param {Object} [opts] Options related to the navigation + * @param {boolean} [opts.replaceState] If `true`, will replace the current `history` entry rather than creating a new one with `pushState` + * @param {boolean} [opts.noScroll] If `true`, the browser will maintain its scroll position rather than scrolling to the top of the page after navigation + * @param {boolean} [opts.keepFocus] If `true`, the currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body + * @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. + * @param {App.PageState} [opts.state] An optional object that will be available on the `$page.state` store + * @returns {Promise} + */ +export function goto(url, opts = {}) { + if (!BROWSER) { + throw new Error('Cannot call goto(...) on the server'); + } - return invalidate(); - }, + url = resolve_url(url); - invalidate_all: () => { - force_invalidation = true; - return invalidate(); - }, + if (url.origin !== origin) { + return Promise.reject( + new Error( + DEV + ? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead` + : 'goto: invalid URL' + ) + ); + } - preload_data: async (href) => { - const url = resolve_url(href); - const intent = get_navigation_intent(url, false); + return _goto(url, opts, 0); +} - if (!intent) { - throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); - } +/** + * 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. + * + * 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). + * To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL. + * + * The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned. + * This can be useful if you want to invalidate based on a pattern instead of a exact match. + * + * ```ts + * // Example: Match '/path' regardless of the query parameters + * import { invalidate } from '$app/navigation'; + * + * invalidate((url) => url.pathname === '/path'); + * ``` + * @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL + * @returns {Promise} + */ +export function invalidate(resource) { + if (!BROWSER) { + throw new Error('Cannot call invalidate(...) on the server'); + } - const result = await preload_data(intent); - if (result.type === 'redirect') { - return { - type: result.type, - location: result.location - }; - } + if (typeof resource === 'function') { + invalidated.push(resource); + } else { + const { href } = new URL(resource, location.href); + invalidated.push((url) => url.href === href); + } - const { status, data } = result.props.page ?? page; - return { type: result.type, status, data }; - }, + return _invalidate(); +} - preload_code: (pathname) => { - if (DEV) { - if (!pathname.startsWith(base)) { - throw new Error( - `pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")` - ); - } +/** + * Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated. + * @returns {Promise} + */ +export function invalidateAll() { + if (!BROWSER) { + throw new Error('Cannot call invalidateAll() on the server'); + } - if (!routes.find((route) => route.exec(get_url_path(pathname)))) { - throw new Error(`'${pathname}' did not match any routes`); - } - } + force_invalidation = true; + return _invalidate(); +} - return preload_code(pathname); - }, +/** + * Programmatically preloads the given page, which means + * 1. ensuring that the code for the page is loaded, and + * 2. calling the page's load function with the appropriate options. + * + * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `` element with `data-sveltekit-preload-data`. + * If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous. + * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete. + * + * @param {string} href Page to preload + * @returns {Promise<{ type: 'loaded'; status: number; data: Record } | { type: 'redirect'; location: string }>} + */ +export async function preloadData(href) { + if (!BROWSER) { + throw new Error('Cannot call preloadData(...) on the server'); + } - push_state: (url, state) => { - if (DEV) { - try { - devalue.stringify(state); - } catch (error) { - // @ts-expect-error - throw new Error(`Could not serialize state${error.path}`); - } - } + const url = resolve_url(href); + const intent = get_navigation_intent(url, false); - update_scroll_positions(current_history_index); - const opts = { - [HISTORY_INDEX]: (current_history_index += 1), - [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href - }; + if (!intent) { + throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); + } - original_push_state.call(history, opts, '', resolve_url(url)); + const result = await _preload_data(intent); + if (result.type === 'redirect') { + return { + type: result.type, + location: result.location + }; + } - page = { ...page, state }; - root.$set({ page }); + const { status, data } = result.props.page ?? page; + return { type: result.type, status, data }; +} - states[current_history_index] = state; - clear_onward_history(current_history_index, current_navigation_index); - }, +/** + * Programmatically imports the code for routes that haven't yet been fetched. + * Typically, you might call this to speed up subsequent navigation. + * + * 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`). + * + * Unlike `preloadData`, this won't call `load` functions. + * Returns a Promise that resolves when the modules have been imported. + * + * @param {string} pathname + * @returns {Promise} + */ +export function preloadCode(pathname) { + if (!BROWSER) { + throw new Error('Cannot call preloadCode(...) on the server'); + } - replace_state: (url, state) => { - if (DEV) { - try { - devalue.stringify(state); - } catch (error) { - // @ts-expect-error - throw new Error(`Could not serialize state${error.path}`); - } - } + if (DEV) { + if (!pathname.startsWith(base)) { + throw new Error( + `pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")` + ); + } - const opts = { - [HISTORY_INDEX]: current_history_index, - [NAVIGATION_INDEX]: current_navigation_index, - [PAGE_URL_KEY]: page.url.href - }; + if (!routes.find((route) => route.exec(get_url_path(pathname)))) { + throw new Error(`'${pathname}' did not match any routes`); + } + } - original_replace_state.call(history, opts, '', resolve_url(url)); + return _preload_code(pathname); +} - page = { ...page, state }; - root.$set({ page }); +/** + * 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). + * + * @param {string | URL} url + * @param {App.PageState} state + * @returns {void} + */ +export function pushState(url, state) { + if (!BROWSER) { + throw new Error('Cannot call pushState(...) on the server'); + } - states[current_history_index] = state; - }, + if (DEV) { + try { + // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances + devalue.stringify(state); + } catch (error) { + // @ts-expect-error + throw new Error(`Could not serialize state${error.path}`); + } + } - apply_action: async (result) => { - if (result.type === 'error') { - const url = new URL(location.href); + update_scroll_positions(current_history_index); - const { branch, route } = current; - if (!route) return; + const opts = { + [HISTORY_INDEX]: (current_history_index += 1), + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href, + [STATES_KEY]: state + }; - const error_load = await load_nearest_error_page( - current.branch.length, - branch, - route.errors - ); - if (error_load) { - const navigation_result = await get_navigation_result_from_branch({ - url, - params: current.params, - branch: branch.slice(0, error_load.idx).concat(error_load.node), - status: result.status ?? 500, - error: result.error, - route - }); + original_push_state.call(history, opts, '', resolve_url(url)); - current = navigation_result.state; + page = { ...page, state }; + root.$set({ page }); - root.$set(navigation_result.props); + clear_onward_history(current_history_index, current_navigation_index); +} - tick().then(reset_focus); - } - } else if (result.type === 'redirect') { - goto(result.location, { invalidateAll: true }, 0); - } else { - /** @type {Record} */ - root.$set({ - // this brings Svelte's view of the world in line with SvelteKit's - // after use:enhance reset the form.... - form: null, - page: { ...page, form: result.data, status: result.status } - }); - - // ...so that setting the `form` prop takes effect and isn't ignored - await tick(); - root.$set({ form: result.data }); - - if (result.type === 'success') { - reset_focus(); - } - } - }, +/** + * 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). + * + * @param {string | URL} url + * @param {App.PageState} state + * @returns {void} + */ +export function replaceState(url, state) { + if (!BROWSER) { + throw new Error('Cannot call replaceState(...) on the server'); + } - _start_router: () => { - history.scrollRestoration = 'manual'; - - // Adopted from Nuxt.js - // Reset scrollRestoration to auto when leaving page, allowing page reload - // and back-navigation from other pages to use the browser to restore the - // scrolling position. - addEventListener('beforeunload', (e) => { - let should_block = false; - - persist_state(); - - if (!navigating) { - const nav = create_navigation(current, undefined, null, 'leave'); - - // If we're navigating, beforeNavigate was already called. If we end up in here during navigation, - // it's due to an external or full-page-reload link, for which we don't want to call the hook again. - /** @type {import('@sveltejs/kit').BeforeNavigate} */ - const navigation = { - ...nav.navigation, - cancel: () => { - should_block = true; - nav.reject(new Error('navigation was cancelled')); - } - }; + if (DEV) { + try { + // use `devalue.stringify` as a convenient way to ensure we exclude values that can't be properly rehydrated, such as custom class instances + devalue.stringify(state); + } catch (error) { + // @ts-expect-error + throw new Error(`Could not serialize state${error.path}`); + } + } - callbacks.before_navigate.forEach((fn) => fn(navigation)); - } + const opts = { + [HISTORY_INDEX]: current_history_index, + [NAVIGATION_INDEX]: current_navigation_index, + [PAGE_URL_KEY]: page.url.href, + [STATES_KEY]: state + }; - if (should_block) { - e.preventDefault(); - e.returnValue = ''; - } else { - history.scrollRestoration = 'auto'; - } - }); + original_replace_state.call(history, opts, '', resolve_url(url)); - addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - persist_state(); - } + page = { ...page, state }; + root.$set({ page }); +} + +/** @type {typeof import('../app/forms.js').applyAction} */ +export async function apply_action(result) { + if (result.type === 'error') { + const url = new URL(location.href); + + const { branch, route } = current; + if (!route) return; + + const error_load = await load_nearest_error_page(current.branch.length, branch, route.errors); + if (error_load) { + const navigation_result = await get_navigation_result_from_branch({ + url, + params: current.params, + branch: branch.slice(0, error_load.idx).concat(error_load.node), + status: result.status ?? 500, + error: result.error, + route }); - // @ts-expect-error this isn't supported everywhere yet - if (!navigator.connection?.saveData) { - setup_preload(); - } + current = navigation_result.state; - /** @param {MouseEvent} event */ - container.addEventListener('click', (event) => { - // Adapted from https://github.com/visionmedia/page.js - // MIT license https://github.com/visionmedia/page.js#license - if (event.button || event.which !== 1) return; - if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; - if (event.defaultPrevented) return; - - const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container); - if (!a) return; - - const { url, external, target, download } = get_link_info(a, base); - if (!url) return; - - // bail out before `beforeNavigate` if link opens in a different tab - if (target === '_parent' || target === '_top') { - if (window.parent !== window) return; - } else if (target && target !== '_self') { - return; - } + root.$set(navigation_result.props); - const options = get_router_options(a); - const is_svg_a_element = a instanceof SVGAElement; - - // Ignore URL protocols that differ to the current one and are not http(s) (e.g. `mailto:`, `tel:`, `myapp:`, etc.) - // This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external - // navigation, but it's not clear how to handle that case and it's not likely to come up in practice. - // MEMO: Without this condition, firefox will open mailer twice. - // See: - // - https://github.com/sveltejs/kit/issues/4045 - // - https://github.com/sveltejs/kit/issues/5725 - // - https://github.com/sveltejs/kit/issues/6496 - if ( - !is_svg_a_element && - url.protocol !== location.protocol && - !(url.protocol === 'https:' || url.protocol === 'http:') - ) - return; + tick().then(reset_focus); + } + } else if (result.type === 'redirect') { + _goto(result.location, { invalidateAll: true }, 0); + } else { + /** @type {Record} */ + root.$set({ + // this brings Svelte's view of the world in line with SvelteKit's + // after use:enhance reset the form.... + form: null, + page: { ...page, form: result.data, status: result.status } + }); - if (download) return; + // ...so that setting the `form` prop takes effect and isn't ignored + await tick(); + root.$set({ form: result.data }); - // Ignore the following but fire beforeNavigate - if (external || options.reload) { - if (before_navigate({ url, type: 'link' })) { - // set `navigating` to `true` to prevent `beforeNavigate` callbacks - // being called when the page unloads - navigating = true; - } else { - event.preventDefault(); - } + if (result.type === 'success') { + reset_focus(); + } + } +} + +function _start_router() { + history.scrollRestoration = 'manual'; + + // Adopted from Nuxt.js + // Reset scrollRestoration to auto when leaving page, allowing page reload + // and back-navigation from other pages to use the browser to restore the + // scrolling position. + addEventListener('beforeunload', (e) => { + let should_block = false; + + persist_state(); - return; + if (!navigating) { + const nav = create_navigation(current, undefined, null, 'leave'); + + // If we're navigating, beforeNavigate was already called. If we end up in here during navigation, + // it's due to an external or full-page-reload link, for which we don't want to call the hook again. + /** @type {import('@sveltejs/kit').BeforeNavigate} */ + const navigation = { + ...nav.navigation, + cancel: () => { + should_block = true; + nav.reject(new Error('navigation was cancelled')); } + }; - // Check if new url only differs by hash and use the browser default behavior in that case - // This will ensure the `hashchange` event is fired - // Removing the hash does a full page navigation in the browser, so make sure a hash is present - const [nonhash, hash] = url.href.split('#'); - if (hash !== undefined && nonhash === strip_hash(location)) { - // If we are trying to navigate to the same hash, we should only - // attempt to scroll to that element and avoid any history changes. - // Otherwise, this can cause Firefox to incorrectly assign a null - // history state value without any signal that we can detect. - const [, current_hash] = current.url.href.split('#'); - if (current_hash === hash) { - event.preventDefault(); - - // We're already on /# and click on a link that goes to /#, or we're on - // /#top and click on a link that goes to /#top. In those cases just go to - // the top of the page, and avoid a history change. - if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) { - window.scrollTo({ top: 0 }); - } else { - a.ownerDocument.getElementById(hash)?.scrollIntoView(); - } + callbacks.before_navigate.forEach((fn) => fn(navigation)); + } - return; - } - // set this flag to distinguish between navigations triggered by - // clicking a hash link and those triggered by popstate - hash_navigating = true; + if (should_block) { + e.preventDefault(); + e.returnValue = ''; + } else { + history.scrollRestoration = 'auto'; + } + }); - update_scroll_positions(current_history_index); + addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + persist_state(); + } + }); + + // @ts-expect-error this isn't supported everywhere yet + if (!navigator.connection?.saveData) { + setup_preload(); + } - update_url(url); + /** @param {MouseEvent} event */ + container.addEventListener('click', (event) => { + // Adapted from https://github.com/visionmedia/page.js + // MIT license https://github.com/visionmedia/page.js#license + if (event.button || event.which !== 1) return; + if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + if (event.defaultPrevented) return; - if (!options.replace_state) return; + const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container); + if (!a) return; - // hashchange event shouldn't occur if the router is replacing state. - hash_navigating = false; - } + const { url, external, target, download } = get_link_info(a, base); + if (!url) return; + // bail out before `beforeNavigate` if link opens in a different tab + if (target === '_parent' || target === '_top') { + if (window.parent !== window) return; + } else if (target && target !== '_self') { + return; + } + + const options = get_router_options(a); + const is_svg_a_element = a instanceof SVGAElement; + + // Ignore URL protocols that differ to the current one and are not http(s) (e.g. `mailto:`, `tel:`, `myapp:`, etc.) + // This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external + // navigation, but it's not clear how to handle that case and it's not likely to come up in practice. + // MEMO: Without this condition, firefox will open mailer twice. + // See: + // - https://github.com/sveltejs/kit/issues/4045 + // - https://github.com/sveltejs/kit/issues/5725 + // - https://github.com/sveltejs/kit/issues/6496 + if ( + !is_svg_a_element && + url.protocol !== location.protocol && + !(url.protocol === 'https:' || url.protocol === 'http:') + ) + return; + + if (download) return; + + // Ignore the following but fire beforeNavigate + if (external || options.reload) { + if (_before_navigate({ url, type: 'link' })) { + // set `navigating` to `true` to prevent `beforeNavigate` callbacks + // being called when the page unloads + navigating = true; + } else { event.preventDefault(); + } - navigate({ - type: 'link', - url, - keepfocus: options.keepfocus, - noscroll: options.noscroll, - replace_state: options.replace_state ?? url.href === location.href - }); - }); + return; + } - container.addEventListener('submit', (event) => { - if (event.defaultPrevented) return; + // Check if new url only differs by hash and use the browser default behavior in that case + // This will ensure the `hashchange` event is fired + // Removing the hash does a full page navigation in the browser, so make sure a hash is present + const [nonhash, hash] = url.href.split('#'); + if (hash !== undefined && nonhash === strip_hash(location)) { + // If we are trying to navigate to the same hash, we should only + // attempt to scroll to that element and avoid any history changes. + // Otherwise, this can cause Firefox to incorrectly assign a null + // history state value without any signal that we can detect. + const [, current_hash] = current.url.href.split('#'); + if (current_hash === hash) { + event.preventDefault(); - const form = /** @type {HTMLFormElement} */ ( - HTMLFormElement.prototype.cloneNode.call(event.target) - ); + // We're already on /# and click on a link that goes to /#, or we're on + // /#top and click on a link that goes to /#top. In those cases just go to + // the top of the page, and avoid a history change. + if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) { + window.scrollTo({ top: 0 }); + } else { + a.ownerDocument.getElementById(hash)?.scrollIntoView(); + } - const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ ( - event.submitter - ); + return; + } + // set this flag to distinguish between navigations triggered by + // clicking a hash link and those triggered by popstate + hash_navigating = true; - const method = submitter?.formMethod || form.method; + update_scroll_positions(current_history_index); - if (method !== 'get') return; + update_url(url); - const url = new URL( - (submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action - ); + if (!options.replace_state) return; - if (is_external_url(url, base)) return; + // hashchange event shouldn't occur if the router is replacing state. + hash_navigating = false; + } - const event_form = /** @type {HTMLFormElement} */ (event.target); + event.preventDefault(); - const options = get_router_options(event_form); - if (options.reload) return; + navigate({ + type: 'link', + url, + keepfocus: options.keepfocus, + noscroll: options.noscroll, + replace_state: options.replace_state ?? url.href === location.href + }); + }); - event.preventDefault(); - event.stopPropagation(); + container.addEventListener('submit', (event) => { + if (event.defaultPrevented) return; - const data = new FormData(event_form); + const form = /** @type {HTMLFormElement} */ ( + HTMLFormElement.prototype.cloneNode.call(event.target) + ); - const submitter_name = submitter?.getAttribute('name'); - if (submitter_name) { - data.append(submitter_name, submitter?.getAttribute('value') ?? ''); - } + const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ (event.submitter); - // @ts-expect-error `URLSearchParams(fd)` is kosher, but typescript doesn't know that - url.search = new URLSearchParams(data).toString(); + const method = submitter?.formMethod || form.method; - navigate({ - type: 'form', - url, - keepfocus: options.keepfocus, - noscroll: options.noscroll, - replace_state: options.replace_state ?? url.href === location.href - }); - }); + if (method !== 'get') return; - addEventListener('popstate', async (event) => { - if (event.state?.[HISTORY_INDEX]) { - const history_index = event.state[HISTORY_INDEX]; - token = {}; - - // if a popstate-driven navigation is cancelled, we need to counteract it - // with history.go, which means we end up back here, hence this check - if (history_index === current_history_index) return; - - const scroll = scroll_positions[history_index]; - const state = states[history_index] ?? {}; - const url = new URL(event.state[PAGE_URL_KEY] ?? location.href); - const navigation_index = event.state[NAVIGATION_INDEX]; - const is_hash_change = strip_hash(location) === strip_hash(current.url); - const shallow = - navigation_index === current_navigation_index && (has_navigated || is_hash_change); - - if (shallow) { - // We don't need to navigate, we just need to update scroll and/or state. - // This happens with hash links and `pushState`/`replaceState`. The - // exception is if we haven't navigated yet, since we could have - // got here after a modal navigation then a reload - update_url(url); - - scroll_positions[current_history_index] = scroll_state(); - if (scroll) scrollTo(scroll.x, scroll.y); - - if (state !== page.state) { - page = { ...page, state }; - root.$set({ page }); - } + const url = new URL( + (submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action + ); - current_history_index = history_index; - return; - } + if (is_external_url(url, base)) return; - const delta = history_index - current_history_index; + const event_form = /** @type {HTMLFormElement} */ (event.target); - await navigate({ - type: 'popstate', - url, - popped: { - state, - scroll, - delta - }, - accept: () => { - current_history_index = history_index; - current_navigation_index = navigation_index; - }, - block: () => { - history.go(-delta); - }, - nav_token: token - }); - } else { - // since popstate event is also emitted when an anchor referencing the same - // document is clicked, we have to check that the router isn't already handling - // the navigation. otherwise we would be updating the page store twice. - if (!hash_navigating) { - const url = new URL(location.href); - update_url(url); - } - } - }); + const options = get_router_options(event_form); + if (options.reload) return; - addEventListener('hashchange', () => { - // if the hashchange happened as a result of clicking on a link, - // we need to update history, otherwise we have to leave it alone - if (hash_navigating) { - hash_navigating = false; - original_replace_state.call( - history, - { - ...history.state, - [HISTORY_INDEX]: ++current_history_index, - [NAVIGATION_INDEX]: current_navigation_index - }, - '', - location.href - ); - } - }); + event.preventDefault(); + event.stopPropagation(); - // fix link[rel=icon], because browsers will occasionally try to load relative - // URLs after a pushState/replaceState, resulting in a 404 — see - // https://github.com/sveltejs/kit/issues/3748#issuecomment-1125980897 - for (const link of document.querySelectorAll('link')) { - if (link.rel === 'icon') link.href = link.href; // eslint-disable-line - } + const data = new FormData(event_form); - addEventListener('pageshow', (event) => { - // If the user navigates to another site and then uses the back button and - // bfcache hits, we need to set navigating to null, the site doesn't know - // the navigation away from it was successful. - // Info about bfcache here: https://web.dev/bfcache - if (event.persisted) { - stores.navigating.set(null); + const submitter_name = submitter?.getAttribute('name'); + if (submitter_name) { + data.append(submitter_name, submitter?.getAttribute('value') ?? ''); + } + + // @ts-expect-error `URLSearchParams(fd)` is kosher, but typescript doesn't know that + url.search = new URLSearchParams(data).toString(); + + navigate({ + type: 'form', + url, + keepfocus: options.keepfocus, + noscroll: options.noscroll, + replace_state: options.replace_state ?? url.href === location.href + }); + }); + + addEventListener('popstate', async (event) => { + if (event.state?.[HISTORY_INDEX]) { + const history_index = event.state[HISTORY_INDEX]; + token = {}; + + // if a popstate-driven navigation is cancelled, we need to counteract it + // with history.go, which means we end up back here, hence this check + if (history_index === current_history_index) return; + + const scroll = scroll_positions[history_index]; + const state = event.state[STATES_KEY] ?? {}; + const url = new URL(event.state[PAGE_URL_KEY] ?? location.href); + const navigation_index = event.state[NAVIGATION_INDEX]; + const is_hash_change = strip_hash(location) === strip_hash(current.url); + const shallow = + navigation_index === current_navigation_index && (has_navigated || is_hash_change); + + if (shallow) { + // We don't need to navigate, we just need to update scroll and/or state. + // This happens with hash links and `pushState`/`replaceState`. The + // exception is if we haven't navigated yet, since we could have + // got here after a modal navigation then a reload + update_url(url); + + scroll_positions[current_history_index] = scroll_state(); + if (scroll) scrollTo(scroll.x, scroll.y); + + if (state !== page.state) { + page = { ...page, state }; + root.$set({ page }); } - }); - /** - * @param {URL} url - */ - function update_url(url) { - current.url = url; - stores.page.set({ ...page, url }); - stores.page.notify(); + current_history_index = history_index; + return; } - }, - _hydrate: async ({ - status = 200, - error, - node_ids, - params, - route, - data: server_data_nodes, - form - }) => { - hydrated = true; + const delta = history_index - current_history_index; - const url = new URL(location.href); - - if (!__SVELTEKIT_EMBEDDED__) { - // See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation - // of determining the params on the client side. - ({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {}); + await navigate({ + type: 'popstate', + url, + popped: { + state, + scroll, + delta + }, + accept: () => { + current_history_index = history_index; + current_navigation_index = navigation_index; + }, + block: () => { + history.go(-delta); + }, + nav_token: token + }); + } else { + // since popstate event is also emitted when an anchor referencing the same + // document is clicked, we have to check that the router isn't already handling + // the navigation. otherwise we would be updating the page store twice. + if (!hash_navigating) { + const url = new URL(location.href); + update_url(url); } + } + }); - /** @type {import('./types.js').NavigationFinished | undefined} */ - let result; + addEventListener('hashchange', () => { + // if the hashchange happened as a result of clicking on a link, + // we need to update history, otherwise we have to leave it alone + if (hash_navigating) { + hash_navigating = false; + original_replace_state.call( + history, + { + ...history.state, + [HISTORY_INDEX]: ++current_history_index, + [NAVIGATION_INDEX]: current_navigation_index + }, + '', + location.href + ); + } + }); - try { - const branch_promises = node_ids.map(async (n, i) => { - const server_data_node = server_data_nodes[i]; - // Type isn't completely accurate, we still need to deserialize uses - if (server_data_node?.uses) { - server_data_node.uses = deserialize_uses(server_data_node.uses); - } + // fix link[rel=icon], because browsers will occasionally try to load relative + // URLs after a pushState/replaceState, resulting in a 404 — see + // https://github.com/sveltejs/kit/issues/3748#issuecomment-1125980897 + for (const link of document.querySelectorAll('link')) { + if (link.rel === 'icon') link.href = link.href; // eslint-disable-line + } - return load_node({ - loader: app.nodes[n], - url, - params, - route, - parent: async () => { - const data = {}; - for (let j = 0; j < i; j += 1) { - Object.assign(data, (await branch_promises[j]).data); - } - return data; - }, - server_data_node: create_data_node(server_data_node) - }); - }); + addEventListener('pageshow', (event) => { + // If the user navigates to another site and then uses the back button and + // bfcache hits, we need to set navigating to null, the site doesn't know + // the navigation away from it was successful. + // Info about bfcache here: https://web.dev/bfcache + if (event.persisted) { + stores.navigating.set(null); + } + }); + + /** + * @param {URL} url + */ + function update_url(url) { + current.url = url; + stores.page.set({ ...page, url }); + stores.page.notify(); + } +} - /** @type {Array} */ - const branch = await Promise.all(branch_promises); +/** + * @param {HTMLElement} target + * @param {{ + * status: number; + * error: App.Error | null; + * node_ids: number[]; + * params: Record; + * route: { id: string | null }; + * data: Array; + * form: Record | null; + * }} opts + */ +async function _hydrate( + target, + { status = 200, error, node_ids, params, route, data: server_data_nodes, form } +) { + hydrated = true; + + const url = new URL(location.href); + + if (!__SVELTEKIT_EMBEDDED__) { + // See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation + // of determining the params on the client side. + ({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {}); + } - const parsed_route = routes.find(({ id }) => id === route.id); + /** @type {import('./types.js').NavigationFinished | undefined} */ + let result; - // server-side will have compacted the branch, reinstate empty slots - // so that error boundaries can be lined up correctly - if (parsed_route) { - const layouts = parsed_route.layouts; - for (let i = 0; i < layouts.length; i++) { - if (!layouts[i]) { - branch.splice(i, 0, undefined); - } + try { + const branch_promises = node_ids.map(async (n, i) => { + const server_data_node = server_data_nodes[i]; + // Type isn't completely accurate, we still need to deserialize uses + if (server_data_node?.uses) { + server_data_node.uses = deserialize_uses(server_data_node.uses); + } + + return load_node({ + loader: app.nodes[n], + url, + params, + route, + parent: async () => { + const data = {}; + for (let j = 0; j < i; j += 1) { + Object.assign(data, (await branch_promises[j]).data); } - } + return data; + }, + server_data_node: create_data_node(server_data_node) + }); + }); - result = await get_navigation_result_from_branch({ - url, - params, - branch, - status, - error, - form, - route: parsed_route ?? null - }); - } catch (error) { - if (error instanceof Redirect) { - // this is a real edge case — `load` would need to return - // a redirect but only in the browser - await native_navigation(new URL(error.location, location.href)); - return; - } + /** @type {Array} */ + const branch = await Promise.all(branch_promises); - result = await load_root_error_page({ - status: get_status(error), - error: await handle_error(error, { url, params, route }), - url, - route - }); - } + const parsed_route = routes.find(({ id }) => id === route.id); - if (result.props.page) { - result.props.page.state = {}; + // server-side will have compacted the branch, reinstate empty slots + // so that error boundaries can be lined up correctly + if (parsed_route) { + const layouts = parsed_route.layouts; + for (let i = 0; i < layouts.length; i++) { + if (!layouts[i]) { + branch.splice(i, 0, undefined); + } } + } - initialize(result); + result = await get_navigation_result_from_branch({ + url, + params, + branch, + status, + error, + form, + route: parsed_route ?? null + }); + } catch (error) { + if (error instanceof Redirect) { + // this is a real edge case — `load` would need to return + // a redirect but only in the browser + await native_navigation(new URL(error.location, location.href)); + return; } - }; + + result = await load_root_error_page({ + status: get_status(error), + error: await handle_error(error, { url, params, route }), + url, + route + }); + } + + if (result.props.page) { + result.props.page.state = {}; + } + + initialize(result, target); } /** diff --git a/packages/kit/src/runtime/client/fetcher.js b/packages/kit/src/runtime/client/fetcher.js index 6692ea13eb46..57c664318c2d 100644 --- a/packages/kit/src/runtime/client/fetcher.js +++ b/packages/kit/src/runtime/client/fetcher.js @@ -1,9 +1,10 @@ -import { DEV } from 'esm-env'; +import { BROWSER, DEV } from 'esm-env'; import { hash } from '../hash.js'; let loading = 0; -export const native_fetch = window.fetch; +/** @type {typeof fetch} */ +export const native_fetch = BROWSER ? window.fetch : /** @type {any} */ (() => {}); export function lock_fetch() { loading += 1; @@ -13,7 +14,7 @@ export function unlock_fetch() { loading -= 1; } -if (DEV) { +if (DEV && BROWSER) { let can_inspect_stack_trace = false; const check_stack_trace = async () => { @@ -61,7 +62,7 @@ if (DEV) { return native_fetch(input, init); }; -} else { +} else if (BROWSER) { window.fetch = (input, init) => { const method = input instanceof Request ? input.method : init?.method || 'GET'; diff --git a/packages/kit/src/runtime/client/singletons.js b/packages/kit/src/runtime/client/singletons.js index aacaa04907ec..6c77bcb10565 100644 --- a/packages/kit/src/runtime/client/singletons.js +++ b/packages/kit/src/runtime/client/singletons.js @@ -1,53 +1,5 @@ import { writable } from 'svelte/store'; import { create_updated_store, notifiable_store } from './utils.js'; -import { BROWSER } from 'esm-env'; - -/** @type {import('./types.js').Client} */ -export let client; - -/** - * @param {{ - * client: import('./types.js').Client; - * }} opts - */ -export function init(opts) { - client = opts.client; -} - -/** - * @template {keyof typeof client} T - * @param {T} key - * @returns {typeof client[T]} - */ -export function client_method(key) { - if (!BROWSER) { - if ( - key === 'before_navigate' || - key === 'after_navigate' || - key === 'on_navigate' || - key === 'push_state' || - key === 'replace_state' - ) { - // @ts-expect-error doesn't recognize that both keys here return void so expects a async function - return () => {}; - } else { - /** @type {Record} */ - const name_lookup = { - disable_scroll_handling: 'disableScrollHandling', - preload_data: 'preloadData', - preload_code: 'preloadCode', - invalidate_all: 'invalidateAll' - }; - - return () => { - throw new Error(`Cannot call ${name_lookup[key] ?? key}(...) on the server`); - }; - } - } else { - // @ts-expect-error - return (...args) => client[key](...args); - } -} export const stores = { url: /* @__PURE__ */ notifiable_store({}), diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js index 0ef12e627131..d5c54b77632a 100644 --- a/packages/kit/src/runtime/client/start.js +++ b/packages/kit/src/runtime/client/start.js @@ -1,28 +1,17 @@ import { DEV } from 'esm-env'; import { create_client } from './client.js'; -import { init } from './singletons.js'; /** * @param {import('./types.js').SvelteKitApp} app * @param {HTMLElement} target - * @param {Parameters[0]} [hydrate] + * @param {any} [hydrate] (see _hydrate in client.js for the type) */ -export async function start(app, target, hydrate) { +export function start(app, target, hydrate) { if (DEV && target === document.body) { console.warn( 'Placing %sveltekit.body% directly inside 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
\n %sveltekit.body%\n
' ); } - const client = create_client(app, target); - - init({ client }); - - if (hydrate) { - await client._hydrate(hydrate); - } else { - client.goto(location.href, { replaceState: true }); - } - - client._start_router(); + create_client(app, target, hydrate); } diff --git a/packages/kit/src/runtime/client/types.d.ts b/packages/kit/src/runtime/client/types.d.ts index 458c36ddddae..fcf2f48b939b 100644 --- a/packages/kit/src/runtime/client/types.d.ts +++ b/packages/kit/src/runtime/client/types.d.ts @@ -42,34 +42,6 @@ export interface SvelteKitApp { root: typeof SvelteComponent; } -export interface Client { - // public API, exposed via $app/navigation - after_navigate: typeof afterNavigate; - before_navigate: typeof beforeNavigate; - on_navigate: typeof onNavigate; - disable_scroll_handling(): void; - goto: typeof goto; - invalidate: typeof invalidate; - invalidate_all: typeof invalidateAll; - preload_code: typeof preloadCode; - preload_data: typeof preloadData; - push_state: typeof pushState; - replace_state: typeof replaceState; - apply_action: typeof applyAction; - - // private API - _hydrate(opts: { - status: number; - error: App.Error | null; - node_ids: number[]; - params: Record; - route: { id: string | null }; - data: Array; - form: Record | null; - }): Promise; - _start_router(): void; -} - export type NavigationIntent = { /** `url.pathname + url.search` */ id: string; diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 3f02f49dae6d..d8a88b2a8583 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -1934,11 +1934,39 @@ declare module '$app/forms' { } declare module '$app/navigation' { + /** + * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. + * + * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * */ + export function afterNavigate(callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void): void; + /** + * 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. + * + * 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. + * + * 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`. + * + * 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`. + * + * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * */ + export function beforeNavigate(callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void): void; + /** + * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. + * + * 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. + * + * 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. + * + * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. + * */ + export function onNavigate(callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise): void; /** * 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. * This is generally discouraged, since it breaks user expectations. * */ - export const disableScrollHandling: () => void; + export function disableScrollHandling(): void; /** * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `url`. * For external URLs, use `window.location = url` instead of calling `goto(url)`. @@ -1946,13 +1974,13 @@ declare module '$app/navigation' { * @param 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. * @param {Object} opts Options related to the navigation * */ - export const goto: (url: string | URL, opts?: { - replaceState?: boolean; - noScroll?: boolean; - keepFocus?: boolean; - invalidateAll?: boolean; - state?: App.PageState; - }) => Promise; + export function goto(url: string | URL, opts?: { + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + state?: App.PageState | undefined; + } | undefined): Promise; /** * 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. * @@ -1968,13 +1996,13 @@ declare module '$app/navigation' { * * invalidate((url) => url.pathname === '/path'); * ``` - * @param url The invalidated URL + * @param resource The invalidated URL * */ - export const invalidate: (url: string | URL | ((url: URL) => boolean)) => Promise; + export function invalidate(resource: string | URL | ((url: URL) => boolean)): Promise; /** * Causes all `load` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated. * */ - export const invalidateAll: () => Promise; + export function invalidateAll(): Promise; /** * Programmatically preloads the given page, which means * 1. ensuring that the code for the page is loaded, and @@ -1986,7 +2014,14 @@ declare module '$app/navigation' { * * @param href Page to preload * */ - export const preloadData: (href: string) => Promise>; + export function preloadData(href: string): Promise<{ + type: 'loaded'; + status: number; + data: Record; + } | { + type: 'redirect'; + location: string; + }>; /** * Programmatically imports the code for routes that haven't yet been fetched. * Typically, you might call this to speed up subsequent navigation. @@ -1997,45 +2032,17 @@ declare module '$app/navigation' { * Returns a Promise that resolves when the modules have been imported. * * */ - export const preloadCode: (url: string) => Promise; - /** - * 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. - * - * 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. - * - * 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`. - * - * 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`. - * - * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted. - * */ - export const beforeNavigate: (callback: (navigation: import('@sveltejs/kit').BeforeNavigate) => void) => void; - /** - * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations. - * - * 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. - * - * 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. - * - * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted. - * */ - export const onNavigate: (callback: (navigation: import('@sveltejs/kit').OnNavigate) => MaybePromise<(() => void) | void>) => void; - /** - * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a new URL. - * - * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted. - * */ - export const afterNavigate: (callback: (navigation: import('@sveltejs/kit').AfterNavigate) => void) => void; + export function preloadCode(pathname: string): Promise; /** * 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). * * */ - export const pushState: (url: string | URL, state: App.PageState) => void; + export function pushState(url: string | URL, state: App.PageState): void; /** * 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). * * */ - export const replaceState: (url: string | URL, state: App.PageState) => void; + export function replaceState(url: string | URL, state: App.PageState): void; type MaybePromise = T | Promise; }