in kit/svelteKitCustomClient/client.js [84:1868]
export function create_client(app, target) {
const routes = parse(app);
const default_layout_loader = app.nodes[0];
const default_error_loader = app.nodes[1];
// 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 container = __SVELTEKIT_EMBEDDED__ ? target : document.documentElement;
/** @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, promise: Promise<import('./types').NavigationResult>} | null} */
let load_cache = null;
const callbacks = {
/** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */
before_navigate: [],
/** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
on_navigate: [],
/** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */
after_navigate: [],
};
/** @type {import('./types').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 navigating = false;
let hash_navigating = false;
let force_invalidation = false;
/** @type {import('svelte').SvelteComponent} */
let root;
// keeping track of the history index in order to prevent popstate navigation events if needed
let current_history_index = history.state?.[INDEX_KEY];
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 = Date.now();
// create initial history entry, so we can return here
history.replaceState(
{ ...history.state, [INDEX_KEY]: current_history_index },
"",
location.href
);
}
// 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);
}
/** @type {import('@sveltejs/kit').Page} */
let page;
/** @type {{}} */
let token;
/** @type {Promise<void> | null} */
let pending_invalidate;
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.
pending_invalidate = pending_invalidate || Promise.resolve();
await pending_invalidate;
if (!pending_invalidate) return;
pending_invalidate = null;
const url = new URL(location.href);
const intent = get_navigation_intent(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 nav_token = (token = {});
const navigation_result = intent && (await load_route(intent));
if (nav_token !== token) return;
if (navigation_result) {
if (navigation_result.type === "redirect") {
return goto(new URL(navigation_result.location, url).href, {}, [url.pathname], nav_token);
} else {
if (navigation_result.props.page !== undefined) {
page = navigation_result.props.page;
}
root.$set(navigation_result.props);
}
}
}
/** @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_history_index);
storage.set(SNAPSHOT_KEY, snapshots);
}
/**
* @param {string | URL} url
* @param {{ noScroll?: boolean; replaceState?: boolean; keepFocus?: boolean; state?: any; invalidateAll?: boolean }} opts
* @param {string[]} redirect_chain
* @param {{}} [nav_token]
*/
async function goto(
url,
{
noScroll = false,
replaceState = false,
keepFocus = false,
state = {},
invalidateAll = false,
},
redirect_chain,
nav_token
) {
if (typeof url === "string") {
url = new URL(url, get_base_uri(document));
}
return navigate({
url,
scroll: noScroll ? scroll_state() : null,
keepfocus: keepFocus,
redirect_chain,
details: {
state,
replaceState,
},
nav_token,
accepted: () => {
if (invalidateAll) {
force_invalidation = true;
}
},
blocked: () => {},
type: "goto",
});
}
/** @param {import('./types').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;
}
/** @param {...string} pathnames */
async function preload_code(...pathnames) {
const matching = routes.filter((route) => pathnames.some((pathname) => route.exec(pathname)));
const promises = matching.map((r) => {
return Promise.all([...r.layouts, r.leaf].map((load) => load?.[1]()));
});
await Promise.all(promises);
}
/** @param {import('./types').NavigationFinished} result */
function initialize(result) {
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();
page = /** @type {import('@sveltejs/kit').Page} */ (result.props.page);
root = new app.root({
target,
props: { ...result.props, stores, components },
hydrate: true,
});
restore_snapshot(current_history_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));
started = true;
}
/**
*
* @param {{
* url: URL;
* params: Record<string, string>;
* branch: Array<import('./types').BranchNode | undefined>;
* status: number;
* error: App.Error | null;
* route: import('types').CSRRoute | null;
* form?: Record<string, any> | 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').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),
},
};
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,
},
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 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<Record<string, any>>;
* url: URL;
* params: Record<string, string>;
* route: { id: string | null };
* server_data_node: import('./types').DataNode | null;
* }} options
* @returns {Promise<import('./types').BranchNode>}
*/
async function load_node({ loader, parent, url, params, route, server_data_node }) {
/** @type {Record<string, any> | null} */
let data = null;
/** @type {import('types').Uses} */
const uses = {
dependencies: new Set(),
params: new Set(),
parent: false,
route: false,
url: false,
};
const node = await loader();
if (DEV) {
validate_page_exports(node.universal);
}
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) => {
uses.route = true;
return target[/** @type {'id'} */ (key)];
},
}),
params: new Proxy(params, {
get: (target, key) => {
uses.params.add(/** @type {string} */ (key));
return target[/** @type {string} */ (key)];
},
}),
data: server_data_node?.data ?? null,
url: make_trackable(url, () => {
uses.url = true;
}),
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);
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);
}
// 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() {
uses.parent = true;
return parent();
},
};
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;
}
data = data ? await unwrap_promises(data) : 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 {boolean} parent_changed
* @param {boolean} route_changed
* @param {boolean} url_changed
* @param {import('types').Uses | undefined} uses
* @param {Record<string, string>} params
*/
function has_changed(parent_changed, route_changed, url_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 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').DataNode | null} [previous]
* @returns {import('./types').DataNode | null}
*/
function create_data_node(node, previous) {
if (node?.type === "data") return node;
if (node?.type === "skip") return previous ?? null;
return null;
}
/**
* @param {import('./types').NavigationIntent} intent
* @returns {Promise<import('./types').NavigationResult>}
*/
async function load_route({ id, invalidating, url, params, route }) {
if (load_cache?.id === id) {
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 !== current.url.pathname + current.url.search : false;
const route_changed = current.route ? route.id !== current.route.id : false;
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, 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: error instanceof HttpError ? error.status : 500,
error: await handle_error(error, { url, params, route: { id: route.id } }),
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').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, 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').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,
};
}
let status = 500;
/** @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) {
status = err.status;
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);
}
}
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<import('./types').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').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<import('./types').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 !== location.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),
});
/** @type {import('./types').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,
});
}
/**
* @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);
for (const route of routes) {
const params = route.exec(path);
if (params) {
const id = url.pathname + url.search;
/** @type {import('./types').NavigationIntent} */
const intent = { id, invalidating, route, params: decode_params(params), url };
return intent;
}
}
}
/** @param {URL} url */
function get_url_path(url) {
return decode_pathname(url.pathname.slice(base.length) || "/");
}
/**
* @param {{
* url: URL;
* type: import('@sveltejs/kit').Navigation["type"];
* intent?: import('./types').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 was cancelled"));
},
};
if (!navigating) {
// Don't run the event during redirects
callbacks.before_navigate.forEach((fn) => fn(cancellable));
}
return should_block ? null : nav;
}
/**
* @param {{
* url: URL;
* scroll: { x: number, y: number } | null;
* keepfocus: boolean;
* redirect_chain: string[];
* details: {
* replaceState: boolean;
* state: any;
* } | null;
* type: import('@sveltejs/kit').Navigation["type"];
* delta?: number;
* nav_token?: {};
* accepted: () => void;
* blocked: () => void;
* }} opts
*/
async function navigate({
url,
scroll,
keepfocus,
redirect_chain,
details,
type,
delta,
nav_token = {},
accepted,
blocked,
}) {
const originalUrl = new URL(url.href);
const renamedPathname = getHfDocFullPath(url.pathname);
if (renamedPathname) {
url.pathname = renamedPathname;
}
const intent = get_navigation_intent(url, false);
const nav = before_navigate({ url, type, delta, intent });
if (!nav) {
blocked();
return;
}
// store this before calling `accepted()`, which may change the index
const previous_history_index = current_history_index;
accepted();
navigating = true;
if (started) {
stores.navigating.set(nav.navigation);
}
token = nav_token;
let navigation_result = intent && (await load_route(intent));
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 Error(`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 was aborted"));
return false;
}
if (navigation_result.type === "redirect") {
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
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_chain, url.pathname],
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);
}
}
// 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;
update_scroll_positions(previous_history_index);
capture_snapshot(previous_history_index);
// ensure the url pathname matches the page's trailing slash option
if (
navigation_result.props.page?.url &&
navigation_result.props.page.url.pathname !== url.pathname
) {
url.pathname = navigation_result.props.page?.url.pathname;
}
if (details) {
const change = details.replaceState ? 0 : 1;
details.state[INDEX_KEY] = current_history_index += change;
history[details.replaceState ? "replaceState" : "pushState"](details.state, "", originalUrl);
if (!details.replaceState) {
// 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 (snapshots[i] || scroll_positions[i]) {
delete snapshots[i];
delete scroll_positions[i];
i += 1;
}
}
}
// reset preload synchronously after the history state has been set to avoid race conditions
load_cache = null;
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(
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);
// @ts-ignore
callbacks.after_navigate.push(...after_navigate);
}
root.$set(navigation_result.props);
} else {
initialize(navigation_result);
}
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
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);
}
}
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) {
page = navigation_result.props.page;
}
navigating = false;
if (type === "popstate") {
restore_snapshot(current_history_index);
}
nav.fulfil(undefined);
callbacks.after_navigate.forEach((fn) =>
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation))
);
stores.navigating.set(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').NavigationFinished>}
*/
async function server_fallback(url, route, error, status) {
if (url.origin === location.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);
}
/**
* 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(() => {});
}
if (import.meta.hot) {
import.meta.hot.on("vite:beforeUpdate", () => {
if (current.error) location.reload();
});
}
function setup_preload() {
/** @type {NodeJS.Timeout} */
let mousemove_timeout;
container.addEventListener("mousemove", (event) => {
const target = /** @type {Element} */ (event.target);
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
preload(target, 2);
}, 20);
});
/** @param {Event} event */
function tap(event) {
preload(/** @type {Element} */ (event.composedPath()[0]), 1);
}
container.addEventListener("mousedown", tap);
container.addEventListener("touchstart", tap, { passive: true });
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
preload_code(
get_url_path(new URL(/** @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(get_url_path(/** @type {URL} */ (url)));
}
}
}
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;
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) {
preload_code(get_url_path(/** @type {URL} */ (url)));
}
}
}
callbacks.after_navigate.push(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");
}
return (
app.hooks.handleError({ error, event }) ??
/** @type {any} */ ({ message: event.route.id != null ? "Internal Error" : "Not Found" })
);
}
return {
after_navigate: (fn) => {
onMount(() => {
callbacks.after_navigate.push(fn);
return () => {
const i = callbacks.after_navigate.indexOf(fn);
callbacks.after_navigate.splice(i, 1);
};
});
},
before_navigate: (fn) => {
onMount(() => {
callbacks.before_navigate.push(fn);
return () => {
const i = callbacks.before_navigate.indexOf(fn);
callbacks.before_navigate.splice(i, 1);
};
});
},
on_navigate: (fn) => {
onMount(() => {
callbacks.on_navigate.push(fn);
return () => {
const i = callbacks.on_navigate.indexOf(fn);
callbacks.on_navigate.splice(i, 1);
};
});
},
disable_scroll_handling: () => {
if (DEV && started && !updating) {
throw new Error("Can only disable scroll handling during navigation");
}
if (updating || !started) {
autoscroll = false;
}
},
goto: (href, opts = {}) => {
return goto(href, opts, []);
},
invalidate: (resource) => {
if (typeof resource === "function") {
invalidated.push(resource);
} else {
const { href } = new URL(resource, location.href);
invalidated.push((url) => url.href === href);
}
return invalidate();
},
invalidate_all: () => {
force_invalidation = true;
return invalidate();
},
preload_data: async (href) => {
const url = new URL(href, get_base_uri(document));
const intent = get_navigation_intent(url, false);
if (!intent) {
throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`);
}
await preload_data(intent);
},
preload_code,
apply_action: async (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,
});
current = navigation_result.state;
root.$set(navigation_result.props);
tick().then(reset_focus);
}
} else if (result.type === "redirect") {
goto(result.location, { invalidateAll: true }, []);
} else {
/** @type {Record<string, any>} */
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();
}
}
},
_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"));
},
};
callbacks.before_navigate.forEach((fn) => fn(navigation));
}
if (should_block) {
e.preventDefault();
e.returnValue = "";
} else {
history.scrollRestoration = "auto";
}
});
addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
persist_state();
}
});
// @ts-expect-error this isn't supported everywhere yet
if (!navigator.connection?.saveData) {
setup_preload();
}
/** @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: external_, target, download } = get_link_info(a, base);
const renamedPathname = getHfDocFullPath(url.pathname);
const external = renamedPathname ? false : external_;
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();
}
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 === location.href.split("#")[0]) {
// 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.
if (current.url.hash === url.hash) {
event.preventDefault();
a.ownerDocument.getElementById(hash)?.scrollIntoView();
return;
}
// set this flag to distinguish between navigations triggered by
// clicking a hash link and those triggered by popstate
hash_navigating = true;
update_scroll_positions(current_history_index);
update_url(url);
if (!options.replace_state) return;
// hashchange event shouldn't occur if the router is replacing state.
hash_navigating = false;
event.preventDefault();
}
navigate({
url,
scroll: options.noscroll ? scroll_state() : null,
keepfocus: options.keep_focus ?? false,
redirect_chain: [],
details: {
state: {},
replaceState: options.replace_state ?? url.href === location.href,
},
accepted: () => event.preventDefault(),
blocked: () => event.preventDefault(),
type: "link",
});
});
container.addEventListener("submit", (event) => {
if (event.defaultPrevented) return;
const form = /** @type {HTMLFormElement} */ (
HTMLFormElement.prototype.cloneNode.call(event.target)
);
const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ (
event.submitter
);
const method = submitter?.formMethod || form.method;
if (method !== "get") return;
const url = new URL(
(submitter?.hasAttribute("formaction") && submitter?.formAction) || form.action
);
if (is_external_url(url, base)) return;
const event_form = /** @type {HTMLFormElement} */ (event.target);
const { keep_focus, noscroll, reload, replace_state } = get_router_options(event_form);
if (reload) return;
event.preventDefault();
event.stopPropagation();
const data = new FormData(event_form);
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({
url,
scroll: noscroll ? scroll_state() : null,
keepfocus: keep_focus ?? false,
redirect_chain: [],
details: {
state: {},
replaceState: replace_state ?? url.href === location.href,
},
nav_token: {},
accepted: () => {},
blocked: () => {},
type: "form",
});
});
addEventListener("popstate", async (event) => {
if (event.state?.[INDEX_KEY]) {
// 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 (event.state[INDEX_KEY] === current_history_index) return;
const scroll = scroll_positions[event.state[INDEX_KEY]];
// if the only change is the hash, we don't need to do anything...
if (current.url.href.split("#")[0] === location.href.split("#")[0]) {
// ...except handle scroll
scroll_positions[current_history_index] = scroll_state();
current_history_index = event.state[INDEX_KEY];
scrollTo(scroll.x, scroll.y);
return;
}
const delta = event.state[INDEX_KEY] - current_history_index;
await navigate({
url: new URL(location.href),
scroll,
keepfocus: false,
redirect_chain: [],
details: null,
accepted: () => {
current_history_index = event.state[INDEX_KEY];
},
blocked: () => {
history.go(-delta);
},
type: "popstate",
delta,
});
} 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);
}
}
});
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;
history.replaceState(
{ ...history.state, [INDEX_KEY]: ++current_history_index },
"",
location.href
);
}
});
// 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
}
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();
}
},
_hydrate: async ({
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) || {});
}
/** @type {import('./types').NavigationFinished | undefined} */
let result;
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),
});
});
/** @type {Array<import('./types').BranchNode | undefined>} */
const branch = await Promise.all(branch_promises);
const parsed_route = routes.find(({ id }) => id === route.id);
// 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);
}
}
}
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: error instanceof HttpError ? error.status : 500,
error: await handle_error(error, { url, params, route }),
url,
route,
});
}
initialize(result);
},
};
}