@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
1,841 lines (1,570 loc) • 85.6 kB
JavaScript
import { BROWSER, DEV } from 'esm-env';
import { onMount, tick } from 'svelte';
import {
decode_params,
decode_pathname,
strip_hash,
make_trackable,
normalize_path
} from '../../utils/url.js';
import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
import { parse, parse_server_route } from './parse.js';
import * as storage from './session-storage.js';
import {
find_anchor,
resolve_url,
get_link_info,
get_router_options,
is_external_url,
origin,
scroll_state,
notifiable_store,
create_updated_store,
load_css
} from './utils.js';
import { base } from '__sveltekit/paths';
import * as devalue from 'devalue';
import {
HISTORY_INDEX,
NAVIGATION_INDEX,
PRELOAD_PRIORITIES,
SCROLL_KEY,
STATES_KEY,
SNAPSHOT_KEY,
PAGE_URL_KEY
} from './constants.js';
import { validate_page_exports } from '../../utils/exports.js';
import { compact } from '../../utils/array.js';
import { HttpError, Redirect, SvelteKitError } from '../control.js';
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js';
import { get_message, get_status } from '../../utils/error.js';
import { writable } from 'svelte/store';
import { page, update, navigating } from './state.svelte.js';
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
export { load_css };
const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);
let errored = false;
// We track the scroll position associated with each history entry in sessionStorage,
// rather than on history.state itself, because when navigation is driven by
// popstate it's too late to update the scroll position associated with the
// state we're navigating from
/**
* history index -> { x, y }
* @type {Record<number, { x: number; y: number }>}
*/
const scroll_positions = storage.get(SCROLL_KEY) ?? {};
/**
* navigation index -> any
* @type {Record<string, any[]>}
*/
const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
if (DEV && BROWSER) {
let warned = false;
const current_module_url = import.meta.url.split('?')[0]; // remove query params that vite adds to the URL when it is loaded from node_modules
const warn = () => {
if (warned) return;
// Rather than saving a pointer to the original history methods, which would prevent monkeypatching by other libs,
// inspect the stack trace to see if we're being called from within SvelteKit.
let stack = new Error().stack?.split('\n');
if (!stack) return;
if (!stack[0].includes('https:') && !stack[0].includes('http:')) stack = stack.slice(1); // Chrome includes the error message in the stack
stack = stack.slice(2); // remove `warn` and the place where `warn` was called
// Can be falsy if was called directly from an anonymous function
if (stack[0]?.includes(current_module_url)) return;
warned = true;
console.warn(
"Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use the `pushState` and `replaceState` imports from `$app/navigation` instead."
);
};
const push_state = history.pushState;
history.pushState = (...args) => {
warn();
return push_state.apply(history, args);
};
const replace_state = history.replaceState;
history.replaceState = (...args) => {
warn();
return replace_state.apply(history, args);
};
}
export const stores = {
url: /* @__PURE__ */ notifiable_store({}),
page: /* @__PURE__ */ notifiable_store({}),
navigating: /* @__PURE__ */ writable(
/** @type {import('@sveltejs/kit').Navigation | null} */ (null)
),
updated: /* @__PURE__ */ create_updated_store()
};
/** @param {number} index */
function update_scroll_positions(index) {
scroll_positions[index] = scroll_state();
}
/**
* @param {number} current_history_index
* @param {number} current_navigation_index
*/
function clear_onward_history(current_history_index, current_navigation_index) {
// if we navigated back, then pushed a new state, we can
// release memory by pruning the scroll/snapshot lookup
let i = current_history_index + 1;
while (scroll_positions[i]) {
delete scroll_positions[i];
i += 1;
}
i = current_navigation_index + 1;
while (snapshots[i]) {
delete snapshots[i];
i += 1;
}
}
/**
* Loads `href` the old-fashioned way, with a full page reload.
* Returns a `Promise` that never resolves (to prevent any
* subsequent work, e.g. history manipulation, from happening)
* @param {URL} url
*/
function native_navigation(url) {
location.href = url.href;
return new Promise(() => {});
}
/**
* Checks whether a service worker is registered, and if it is,
* tries to update it.
*/
async function update_service_worker() {
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.getRegistration(base || '/');
if (registration) {
await registration.update();
}
}
}
function noop() {}
/** @type {import('types').CSRRoute[]} All routes of the app. Only available when kit.router.resolution=client */
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} */
export let app;
/** @type {Array<((url: URL) => boolean)>} */
const invalidated = [];
/**
* 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 {{id: string, token: {}, promise: Promise<import('./types.js').NavigationResult>} | null} */
let load_cache = null;
/**
* @type {Map<string, Promise<URL>>}
* Cache for client-side rerouting, since it could contain async calls which we want to
* avoid running multiple times which would slow down navigations (e.g. else preloading
* wouldn't help because on navigation it would be called again). Since `reroute` should be
* a pure function (i.e. always return the same) value it's safe to cache across navigations.
* The server reroute calls don't need to be cached because they are called using `import(...)`
* which is cached per the JS spec.
*/
const reroute_cache = new Map();
/**
* Note on before_navigate_callbacks, on_navigate_callbacks and after_navigate_callbacks:
* do not re-assign as some closures keep references to these Sets
*/
/** @type {Set<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */
const before_navigate_callbacks = new Set();
/** @type {Set<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
const on_navigate_callbacks = new Set();
/** @type {Set<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */
const after_navigate_callbacks = new Set();
/** @type {import('./types.js').NavigationState} */
let current = {
branch: [],
error: null,
// @ts-ignore - we need the initial value to be null
url: null
};
/** this being true means we SSR'd */
let hydrated = false;
let started = false;
let autoscroll = true;
let updating = false;
let is_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;
let force_invalidation = false;
/** @type {import('svelte').SvelteComponent} */
let root;
/** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
let current_history_index;
/** @type {number} */
let current_navigation_index;
/** @type {{}} */
let token;
/**
* A set of tokens which are associated to current preloads.
* If a preload becomes a real navigation, it's removed from the set.
* If a preload token is in the set and the preload errors, the error
* handling logic (for example reloading) is skipped.
*/
const preload_tokens = new Set();
/** @type {Promise<void> | null} */
let pending_invalidate;
/**
* @param {import('./types.js').SvelteKitApp} _app
* @param {HTMLElement} _target
* @param {Parameters<typeof _hydrate>[1]} [hydrate]
*/
export async function start(_app, _target, hydrate) {
if (DEV && _target === document.body) {
console.warn(
'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>'
);
}
// detect basic auth credentials in the current URL
// https://github.com/sveltejs/kit/pull/11179
// if so, refresh the page without credentials
if (document.URL !== location.href) {
// eslint-disable-next-line no-self-assign
location.href = location.href;
}
app = _app;
await _app.hooks.init?.();
routes = __SVELTEKIT_CLIENT_ROUTING__ ? 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];
void default_layout_loader();
void default_error_loader();
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
// within the app don't result in data loss
current_history_index = current_navigation_index = Date.now();
// create initial history entry, so we can return here
history.replaceState(
{
...history.state,
[HISTORY_INDEX]: current_history_index,
[NAVIGATION_INDEX]: current_navigation_index
},
''
);
}
// if we reload the page, or Cmd-Shift-T back to it,
// recover scroll position
const scroll = scroll_positions[current_history_index];
if (scroll) {
history.scrollRestoration = 'manual';
scrollTo(scroll.x, scroll.y);
}
if (hydrate) {
await _hydrate(target, hydrate);
} else {
await navigate({
type: 'enter',
url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href),
replace_state: true
});
}
_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;
const nav_token = (token = {});
const intent = await 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;
const navigation_result = intent && (await load_route(intent));
if (!navigation_result || nav_token !== token) return;
if (navigation_result.type === 'redirect') {
return _goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token);
}
if (navigation_result.props.page) {
Object.assign(page, navigation_result.props.page);
}
current = navigation_result.state;
reset_invalidation();
root.$set(navigation_result.props);
update(navigation_result.props.page);
}
function reset_invalidation() {
invalidated.length = 0;
force_invalidation = false;
}
/** @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);
capture_snapshot(current_navigation_index);
storage.set(SNAPSHOT_KEY, snapshots);
}
/**
* @param {string | URL} url
* @param {{ replaceState?: boolean; noScroll?: boolean; keepFocus?: boolean; invalidateAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any> }} 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;
}
if (options.invalidate) {
options.invalidate.forEach(push_invalidated);
}
}
});
}
/** @param {import('./types.js').NavigationIntent} intent */
async function _preload_data(intent) {
// Reuse the existing pending preload if it's for the same navigation.
// Prevents an edge case where same preload is triggered multiple times,
// then a later one is becoming the real navigation and the preload tokens
// get out of sync.
if (intent.id !== load_cache?.id) {
const preload = {};
preload_tokens.add(preload);
load_cache = {
id: intent.id,
token: preload,
promise: load_route({ ...intent, preload }).then((result) => {
preload_tokens.delete(preload);
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;
}
/**
* @param {URL} url
* @returns {Promise<void>}
*/
async function _preload_code(url) {
const route = (await get_navigation_intent(url, false))?.route;
if (route) {
await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]()));
}
}
/**
* @param {import('./types.js').NavigationFinished} result
* @param {HTMLElement} target
* @param {boolean} hydrate
*/
function initialize(result, target, hydrate) {
if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return;
current = result.state;
const style = document.querySelector('style[data-sveltekit]');
if (style) style.remove();
Object.assign(page, /** @type {import('@sveltejs/kit').Page} */ (result.props.page));
root = new app.root({
target,
props: { ...result.props, stores, components },
hydrate,
// @ts-ignore Svelte 5 specific: asynchronously instantiate the component, i.e. don't call flushSync
sync: false
});
restore_snapshot(current_navigation_index);
if (hydrate) {
/** @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()
};
after_navigate_callbacks.forEach((fn) => fn(navigation));
}
started = true;
}
/**
*
* @param {{
* url: URL;
* params: Record<string, string>;
* branch: Array<import('./types.js').BranchNode | undefined>;
* status: number;
* error: App.Error | null;
* route: import('types').CSRRoute | null;
* form?: Record<string, any> | null;
* }} opts
*/
function get_navigation_result_from_branch({ url, params, branch, status, error, route, form }) {
/** @type {import('types').TrailingSlash} */
let slash = 'never';
// 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
if (base && (url.pathname === base || url.pathname === base + '/')) {
slash = 'always';
} else {
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: clone_page(page)
}
};
if (form !== undefined) {
result.props.form = form;
}
let data = {};
let data_changed = !page;
let p = 0;
for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) {
const node = branch[i];
const prev = current.branch[i];
if (node?.data !== prev?.data) data_changed = true;
if (!node) continue;
data = { ...data, ...node.data };
// Only set props if the node actually updated. This prevents needless rerenders.
if (data_changed) {
result.props[`data_${p}`] = data;
}
p += 1;
}
const page_changed =
!current.url ||
url.href !== current.url.href ||
current.error !== error ||
(form !== undefined && form !== page.form) ||
data_changed;
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
};
}
return result;
}
/**
* Call the universal load function of the given node, if it exists.
*
* @param {{
* loader: import('types').CSRPageNodeLoader;
* parent: () => Promise<Record<string, any>>;
* url: URL;
* params: Record<string, string>;
* route: { id: string | null };
* server_data_node: import('./types.js').DataNode | null;
* }} options
* @returns {Promise<import('./types.js').BranchNode>}
*/
async function load_node({ loader, parent, url, params, route, server_data_node }) {
/** @type {Record<string, any> | 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()
};
const node = await loader();
if (DEV) {
validate_page_exports(node.universal);
if (node.universal && app.hash) {
const options = Object.keys(node.universal).filter((o) => o !== 'load');
if (options.length > 0) {
throw new Error(
`Page options are ignored when \`router.type === 'hash'\` (${route.id} has ${options
.filter((o) => o !== 'load')
.map((o) => `'${o}'`)
.join(', ')})`
);
}
}
}
if (node.universal?.load) {
/** @param {string[]} deps */
function depends(...deps) {
for (const dep of deps) {
if (DEV) validate_depends(/** @type {string} */ (route.id), dep);
const { href } = new URL(dep, url);
uses.dependencies.add(href);
}
}
/** @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);
}
},
app.hash
),
async fetch(resource, init) {
if (resource instanceof Request) {
// 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,
// the headers are undefined on the server if the Headers object is empty
// so we need to make sure they are also undefined here if there are no headers
headers: [...resource.headers].length ? resource.headers : undefined,
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
};
}
const { resolved, promise } = resolve_fetch_url(resource, init, url);
if (is_tracking) {
depends(resolved.href);
}
return promise;
},
setHeaders: () => {}, // noop
depends,
parent() {
if (is_tracking) {
uses.parent = true;
}
return parent();
},
untrack(fn) {
is_tracking = false;
try {
return fn();
} finally {
is_tracking = 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;
}
}
return {
node,
loader,
server: server_data_node,
universal: node.universal?.load ? { type: 'data', data, uses } : null,
data: data ?? server_data_node?.data ?? null,
slash: node.universal?.trailingSlash ?? server_data_node?.slash
};
}
/**
* @param {Request | string | URL} input
* @param {RequestInit | undefined} init
* @param {URL} url
*/
function resolve_fetch_url(input, init, url) {
let requested = input instanceof Request ? input.url : input;
// we must fixup relative urls so they are resolved from the target page
const resolved = new URL(requested, url);
// match ssr serialized data url, which is important to find cached responses
if (resolved.origin === url.origin) {
requested = resolved.href.slice(url.origin.length);
}
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
const promise = started
? subsequent_fetch(requested, resolved.href, init)
: initial_fetch(requested, init);
return { resolved, promise };
}
/**
* @param {boolean} parent_changed
* @param {boolean} route_changed
* @param {boolean} url_changed
* @param {Set<string>} search_params_changed
* @param {import('types').Uses | undefined} uses
* @param {Record<string, string>} 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;
}
for (const param of uses.params) {
if (params[param] !== current.params[param]) return true;
}
for (const href of uses.dependencies) {
if (invalidated.some((fn) => fn(new URL(href)))) return true;
}
return false;
}
/**
* @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;
}
/**
* @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());
const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]);
for (const key of changed) {
const old_values = old_url.searchParams.getAll(key);
const new_values = new_url.searchParams.getAll(key);
if (
old_values.every((value) => new_values.includes(value)) &&
new_values.every((value) => old_values.includes(value))
) {
changed.delete(key);
}
}
return changed;
}
/**
* @param {Omit<import('./types.js').NavigationFinished['state'], 'branch'> & { error: App.Error }} opts
* @returns {import('./types.js').NavigationFinished}
*/
function preload_error({ error, url, route, params }) {
return {
type: 'loaded',
state: {
error,
url,
route,
params,
branch: []
},
props: {
page: clone_page(page),
constructors: []
}
};
}
/**
* @param {import('./types.js').NavigationIntent & { preload?: {} }} intent
* @returns {Promise<import('./types.js').NavigationResult>}
*/
async function load_route({ id, invalidating, url, params, route, preload }) {
if (load_cache?.id === id) {
// the preload becomes the real navigation
preload_tokens.delete(load_cache.token);
return load_cache.promise;
}
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 !== get_page_key(current.url) : 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) {
const handled_error = await handle_error(error, { url, params, route: { id } });
if (preload_tokens.has(preload)) {
return preload_error({ error: handled_error, url, params, route });
}
return load_root_error_page({
status: get_status(error),
error: handled_error,
url,
route
});
}
if (server_data.type === 'redirect') {
return server_data;
}
}
const server_data_nodes = server_data?.nodes;
let parent_changed = false;
const branch_promises = loaders.map(async (loader, i) => {
if (!loader) return;
/** @type {import('./types.js').BranchNode | undefined} */
const previous = current.branch[i];
const server_data_node = server_data_nodes?.[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,
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,
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
)
});
});
// if we don't do this, rejections will be unhandled
for (const p of branch_promises) p.catch(() => {});
/** @type {Array<import('./types.js').BranchNode | undefined>} */
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 {
type: 'redirect',
location: err.location
};
}
if (preload_tokens.has(preload)) {
return preload_error({
error: await handle_error(err, { params, url, route: { id: route.id } }),
url,
params,
route
});
}
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) {
// Before reloading, try to update the service worker if it exists
await update_service_worker();
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 get_navigation_result_from_branch({
url,
params,
branch: branch.slice(0, error_load.idx).concat(error_load.node),
status,
error,
route
});
} else {
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);
}
}
return 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<import('./types.js').BranchNode | undefined>} branch Branch to backtrack
* @param {Array<import('types').CSRPageNodeLoader | undefined>} 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 {
continue;
}
}
}
}
/**
* @param {{
* status: number;
* error: App.Error;
* url: URL;
* route: { id: string | null }
* }} opts
* @returns {Promise<import('./types.js').NavigationFinished>}
*/
async function load_root_error_page({ status, error, url, route }) {
/** @type {Record<string, string>} */
const params = {}; // error page does not have params
/** @type {import('types').ServerDataNode | null} */
let server_data_node = null;
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 (
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);
}
}
}
try {
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
};
return get_navigation_result_from_branch({
url,
params,
branch: [root_layout, root_error],
status,
error,
route: null
});
} catch (error) {
if (error instanceof Redirect) {
return _goto(new URL(error.location, location.href), {}, 0);
}
// TODO: this falls back to the server when a server exists, but what about SPA mode?
throw error;
}
}
/**
* Resolve the relative rerouted URL for a client-side navigation
* @param {URL} url
* @returns {Promise<URL | undefined>}
*/
async function get_rerouted_url(url) {
const href = url.href;
if (reroute_cache.has(href)) {
return reroute_cache.get(href);
}
let rerouted;
try {
const promise = (async () => {
// reroute could alter the given URL, so we pass a copy
let rerouted =
(await app.hooks.reroute({
url: new URL(url),
fetch: async (input, init) => {
return resolve_fetch_url(input, init, url).promise;
}
})) ?? url;
if (typeof rerouted === 'string') {
const tmp = new URL(url); // do not mutate the incoming URL
if (app.hash) {
tmp.hash = rerouted;
} else {
tmp.pathname = rerouted;
}
rerouted = tmp;
}
return rerouted;
})();
reroute_cache.set(href, promise);
rerouted = await promise;
} catch (e) {
reroute_cache.delete(href);
if (DEV) {
// in development, print the error...
console.error(e);
// ...and pause execution, since otherwise we will immediately reload the page
debugger; // eslint-disable-line
}
// fall back to native navigation
return;
}
return rerouted;
}
/**
* Resolve the full info (which route, params, etc.) for a client-side navigation from the URL,
* taking the reroute hook into account. If this isn't a client-side-navigation (or the URL is undefined),
* returns undefined.
* @param {URL | undefined} url
* @param {boolean} invalidating
* @returns {Promise<import('./types.js').NavigationIntent | undefined>}
*/
async function get_navigation_intent(url, invalidating) {
if (!url) return;
if (is_external_url(url, base, app.hash)) return;
if (__SVELTEKIT_CLIENT_ROUTING__) {
const rerouted = await get_rerouted_url(url);
if (!rerouted) return;
const path = get_url_path(rerouted);
for (const route of routes) {
const params = route.exec(path);
if (params) {
return {
id: get_page_key(url),
invalidating,
route,
params: decode_params(params),
url
};
}
}
} else {
/** @type {{ route?: import('types').CSRRouteServer, params: Record<string, string>}} */
const { route, params } = await import(
/* @vite-ignore */
add_resolution_suffix(url.pathname)
);
if (!route) return;
return {
id: get_page_key(url),
invalidating,
route: parse_server_route(route, app.nodes),
params,
url
};
}
}
/** @param {URL} url */
function get_url_path(url) {
return (
decode_pathname(
app.hash ? url.hash.replace(/^#/, '').replace(/[?#].+/, '') : url.pathname.slice(base.length)
) || '/'
);
}
/** @param {URL} url */
function get_page_key(url) {
return (app.hash ? url.hash.replace(/^#/, '') : url.pathname) + url.search;
}
/**
* @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);
if (delta !== undefined) {
nav.navigation.delta = delta;
}
const cancellable = {
...nav.navigation,
cancel: () => {
should_block = true;
nav.reject(new Error('navigation cancelled'));
}
};
if (!is_navigating) {
// Don't run the event during redirects
before_navigate_callbacks.forEach((fn) => fn(cancellable));
}
return should_block ? null : nav;
}
/**
* @param {{
* type: import('@sveltejs/kit').NavigationType;
* url: URL;
* popped?: {
* state: Record<string, any>;
* scroll: { x: number, y: number };
* delta: number;
* };
* keepfocus?: boolean;
* noscroll?: boolean;
* replace_state?: boolean;
* state?: Record<string, any>;
* 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 prev_token = token;
token = nav_token;
const intent = await get_navigation_intent(url, false);
const nav =
type === 'enter'
? create_navigation(current, intent, url, type)
: _before_navigate({ url, type, delta: popped?.delta, intent });
if (!nav) {
block();
if (token === nav_token) token = prev_token;
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;
accept();
is_navigating = true;
if (started && nav.navigation.type !== 'enter') {
stores.navigating.set((navigating.current = nav.navigation));
}
let navigation_result = intent && (await load_route(intent));
if (!navigation_result) {
if (is_external_url(url, base, app.hash)) {
if (DEV && app.hash) {
// Special case for hash mode during DEV: If someone accidentally forgets to use a hash for the link,
// they would end up here in an endless loop. Fall back to error page in that case
navigation_result = await server_fallback(
url,
{ id: null },
await handle_error(
new SvelteKitError(
404,
'Not Found',
`Not found: ${url.pathname} (did you forget the hash?)`
),
{
url,
params: {},
route: { id: null }
}
),
404
);
} else {
return await native_navigation(url);
}
} else {
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
);
}
}
// 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 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 }
}),
url,
route: { id: null }
});
} else {
await _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) {
// Before reloading, try to update the service worker if it exists
await update_service_worker();
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
reset_invalidation();
updating = true;
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;
}
state = popped ? popped.state : state;
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),
[STATES_KEY]: state
};
const fn = replace_state ? history.replaceState : history.pushState;
fn.call(history, entry, '', url);
if (!replace_state) {
clear_onward_history(current_history_index, current_navigation_index);
}
}
// reset preload synchronously after the history state has been set to avoid race conditions
load_cache = null;
navigation_result.props.page.state = state;
if (started) {
current = navigation_result.state;
// reset url before updating page store
if (navigation_result.props.page) {
navigation_result.props.page.url = url;
}
const after_navigate = (
await Promise.all(
Array.from(on_navigate_callbacks, (fn) =>
fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation))
)
)
).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');
if (after_navigate.length > 0) {
function cleanup() {
after_navigate.forEach((fn) => {
after_navigate_callbacks.delete(fn);
});
}
after_navigate.push(cleanup);
after_navigate.forEach((fn) => {
after_navigate_callbacks.add(fn);
});
}
root.$set(navigation_result.props);
update(navigation_result.props.page);
has_navigated = true;
} else {
initialize(navigation_result, target, false);
}
const { activeElement } = document;
// need to render the DOM before we can scroll to the rendered elements and do focus management
await tick();
// 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(app.hash ? (url.hash.split('#')[2] ?? '') : 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);
}
}
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;
if (!keepfocus && !changed_focus) {
reset_focus();
}
autoscroll = true;
if (navigation_result.props.page) {
Object.assign(page, navigation_result.props.page);
}
is_navigating = false;
if (type === 'popstate') {
restore_snapshot(current_navigation_index);
}
nav.fulfil(undefined);
after_navigate_callbacks.forEach((fn) =>
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
);
stores.navigating.set((navigating.current = null));
updating = false;
}
/**
* 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<import('./types.js').NavigationFinished>}
*/
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.)'
);
debugger; // eslint-disable-line
}
return await native_navigation(url);
}
if (import.meta.hot) {
import.meta.hot.on('vite:beforeUpdate', () => {
if (current.error) location.reload();
});
}
/** @typedef {(typeof PRELOAD_PRIORITIES)['hover'] | (typeof PRELOAD_PRIORITIES)['tap']} PreloadDataPriority */
function setup_preload() {
/** @type {NodeJS.Timeout} */
let mousemove_timeout;
/** @type {Element} */
let current_a;
/** @type {PreloadDataPriority} */
let current_priority;
container.addEventListener('mousemove', (event) => {
const target = /** @type {Element} */ (event.target);
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
void preload(target, PRELOAD_PRIORITIES.hover);
}, 20);
});
/** @param {Event} event */
function tap(event) {
if (event.defaultPrevented) return;
void preload(/** @type {Element} */ (event.composedPath()[0]), PRELOAD_PRIORITIES.tap);
}
container.addEventListener('mousedown', tap);
container.addEventListener('touchstart', tap, { passive: true });
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
void _preload_code(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href));
observer.unobserve(entry.target);
}
}
},
{ threshold: 0 }
);
/**
* @param {Element} element
* @param {PreloadDataPriority} priority
*/
async function preload(element, priority) {
const a = find_anchor(element, container);
// we don't want to preload data again if the user has already hovered/tapped
const interacted = a === current_a && priority >= current_priority;
if (!a || interacted) return;
const { url, external, download } = get_link_info(a, base, app.hash);
if (external || download) return;
const options = get_router_options(a);
// we don't want to preload data for a page we're already on
const same_url = url && get_page_key(current.url) === get_page_key(url);
if (options.reload || same_url) return;
if (priority <= options.preload_data) {
current_a = a;
// we don't want to preload data again on tap if we've already preloaded it on hover
current_priority = PRELOAD_PRIORITIES.tap;
const intent = await get_navigation_intent(url, false);
if (!intent) return;
if (DEV) {
void _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://svelte.dev/docs/kit/link-options for more info'
);
}
});
} else {
void _preload_data(intent);
}
} else if (priority <= options.preload_code) {
current_a = a;
current_priority = priority;
void _preload_code(/** @type {URL} */ (url));
}
}
function after_navigate() {
observer.disconnect();
for (const a of container.querySelectorAll('a')) {
const { url, external, download } = get_link_info(a, base, app.hash);
if (external || download) 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.eager) {
void _preload_code(/** @type {URL} */ (url));
}
}
}
after_navigate_callbacks.add(after_navigate);
after_navigate();
}
/**
* @param {unknown} error
* @param {import('@sveltejs/kit').NavigationEvent} event
* @returns {import('types').MaybePromise<App.Error>}
*/
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');
}
const status = get_status(error);
const message = get_message(error);
return (
app.hooks.handleError({ error, event, status, message }) ?? /** @type {any} */ ({ message })
);
}
/**
* @template {Function} T
* @param {Set<T>} callbacks
* @param {T} callback
*/
function add_navigation_callback(callbacks, callback) {
onMount(() => {
callbacks.add(callback);
return () => {
callbacks.delete(callback);
};
});
}
/**
* A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a URL.
*
* `afterNavigate` must be called during a component initializa