Ruben Gres
first commit
ec3efd7
raw
history blame
44.6 kB
import { onMount, tick } from 'svelte';
import { writable } from 'svelte/store';
import { assets, set_paths } from '../paths.js';
import Root from '__GENERATED__/root.svelte';
import { components, dictionary, matchers } from '__GENERATED__/client-manifest.js';
import { init } from './singletons.js';
/**
* @param {unknown} err
* @return {Error}
*/
function coalesce_to_error(err) {
return err instanceof Error ||
(err && /** @type {any} */ (err).name && /** @type {any} */ (err).message)
? /** @type {Error} */ (err)
: new Error(JSON.stringify(err));
}
/**
* @param {import('types').LoadOutput} loaded
* @returns {import('types').NormalizedLoadOutput}
*/
function normalize(loaded) {
// TODO remove for 1.0
// @ts-expect-error
if (loaded.fallthrough) {
throw new Error(
'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-matching'
);
}
// TODO remove for 1.0
if ('maxage' in loaded) {
throw new Error('maxage should be replaced with cache: { maxage }');
}
const has_error_status =
loaded.status && loaded.status >= 400 && loaded.status <= 599 && !loaded.redirect;
if (loaded.error || has_error_status) {
const status = loaded.status;
if (!loaded.error && has_error_status) {
return { status: status || 500, error: new Error() };
}
const error = typeof loaded.error === 'string' ? new Error(loaded.error) : loaded.error;
if (!(error instanceof Error)) {
return {
status: 500,
error: new Error(
`"error" property returned from load() must be a string or instance of Error, received type "${typeof error}"`
)
};
}
if (!status || status < 400 || status > 599) {
console.warn('"error" returned from load() without a valid status code — defaulting to 500');
return { status: 500, error };
}
return { status, error };
}
if (loaded.redirect) {
if (!loaded.status || Math.floor(loaded.status / 100) !== 3) {
throw new Error(
'"redirect" property returned from load() must be accompanied by a 3xx status code'
);
}
if (typeof loaded.redirect !== 'string') {
throw new Error('"redirect" property returned from load() must be a string');
}
}
if (loaded.dependencies) {
if (
!Array.isArray(loaded.dependencies) ||
loaded.dependencies.some((dep) => typeof dep !== 'string')
) {
throw new Error('"dependencies" property returned from load() must be of type string[]');
}
}
// TODO remove before 1.0
if (/** @type {any} */ (loaded).context) {
throw new Error(
'You are returning "context" from a load function. ' +
'"context" was renamed to "stuff", please adjust your code accordingly.'
);
}
return /** @type {import('types').NormalizedLoadOutput} */ (loaded);
}
/**
* @param {string} path
* @param {import('types').TrailingSlash} trailing_slash
*/
function normalize_path(path, trailing_slash) {
if (path === '/' || trailing_slash === 'ignore') return path;
if (trailing_slash === 'never') {
return path.endsWith('/') ? path.slice(0, -1) : path;
} else if (trailing_slash === 'always' && !path.endsWith('/')) {
return path + '/';
}
return path;
}
class LoadURL extends URL {
/** @returns {string} */
get hash() {
throw new Error(
'url.hash is inaccessible from load. Consider accessing hash from the page store within the script tag of your component.'
);
}
}
/** @param {HTMLDocument} doc */
function get_base_uri(doc) {
let baseURI = doc.baseURI;
if (!baseURI) {
const baseTags = doc.getElementsByTagName('base');
baseURI = baseTags.length ? baseTags[0].href : doc.URL;
}
return baseURI;
}
function scroll_state() {
return {
x: pageXOffset,
y: pageYOffset
};
}
/** @param {Event} event */
function find_anchor(event) {
const node = event
.composedPath()
.find((e) => e instanceof Node && e.nodeName.toUpperCase() === 'A'); // SVG <a> elements have a lowercase name
return /** @type {HTMLAnchorElement | SVGAElement | undefined} */ (node);
}
/** @param {HTMLAnchorElement | SVGAElement} node */
function get_href(node) {
return node instanceof SVGAElement
? new URL(node.href.baseVal, document.baseURI)
: new URL(node.href);
}
/** @param {any} value */
function notifiable_store(value) {
const store = writable(value);
let ready = true;
function notify() {
ready = true;
store.update((val) => val);
}
/** @param {any} new_value */
function set(new_value) {
ready = false;
store.set(new_value);
}
/** @param {(value: any) => void} run */
function subscribe(run) {
/** @type {any} */
let old_value;
return store.subscribe((new_value) => {
if (old_value === undefined || (ready && new_value !== old_value)) {
run((old_value = new_value));
}
});
}
return { notify, set, subscribe };
}
function create_updated_store() {
const { set, subscribe } = writable(false);
const interval = +(
/** @type {string} */ (import.meta.env.VITE_SVELTEKIT_APP_VERSION_POLL_INTERVAL)
);
const initial = import.meta.env.VITE_SVELTEKIT_APP_VERSION;
/** @type {NodeJS.Timeout} */
let timeout;
async function check() {
if (import.meta.env.DEV || import.meta.env.SSR) return false;
clearTimeout(timeout);
if (interval) timeout = setTimeout(check, interval);
const file = import.meta.env.VITE_SVELTEKIT_APP_VERSION_FILE;
const res = await fetch(`${assets}/${file}`, {
headers: {
pragma: 'no-cache',
'cache-control': 'no-cache'
}
});
if (res.ok) {
const { version } = await res.json();
const updated = version !== initial;
if (updated) {
set(true);
clearTimeout(timeout);
}
return updated;
} else {
throw new Error(`Version check failed: ${res.status}`);
}
}
if (interval) timeout = setTimeout(check, interval);
return {
subscribe,
check
};
}
/**
* Hash using djb2
* @param {import('types').StrictBody} value
*/
function hash(value) {
let hash = 5381;
let i = value.length;
if (typeof value === 'string') {
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
} else {
while (i) hash = (hash * 33) ^ value[--i];
}
return (hash >>> 0).toString(36);
}
let loading = 0;
const native_fetch = window.fetch;
function lock_fetch() {
loading += 1;
}
function unlock_fetch() {
loading -= 1;
}
if (import.meta.env.DEV) {
let can_inspect_stack_trace = false;
const check_stack_trace = async () => {
const stack = /** @type {string} */ (new Error().stack);
can_inspect_stack_trace = stack.includes('check_stack_trace');
};
check_stack_trace();
window.fetch = (input, init) => {
const url = input instanceof Request ? input.url : input.toString();
const stack = /** @type {string} */ (new Error().stack);
const heuristic = can_inspect_stack_trace ? stack.includes('load_node') : loading;
if (heuristic) {
console.warn(
`Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://kit.svelte.dev/docs/loading#input-fetch`
);
}
return native_fetch(input, init);
};
}
/**
* @param {RequestInfo} resource
* @param {RequestInit} [opts]
*/
function initial_fetch(resource, opts) {
const url = JSON.stringify(typeof resource === 'string' ? resource : resource.url);
let selector = `script[sveltekit\\:data-type="data"][sveltekit\\:data-url=${url}]`;
if (opts && typeof opts.body === 'string') {
selector += `[sveltekit\\:data-body="${hash(opts.body)}"]`;
}
const script = document.querySelector(selector);
if (script && script.textContent) {
const { body, ...init } = JSON.parse(script.textContent);
return Promise.resolve(new Response(body, init));
}
return native_fetch(resource, opts);
}
const param_pattern = /^(\.\.\.)?(\w+)(?:=(\w+))?$/;
/** @param {string} id */
function parse_route_id(id) {
/** @type {string[]} */
const names = [];
/** @type {string[]} */
const types = [];
// `/foo` should get an optional trailing slash, `/foo.json` should not
// const add_trailing_slash = !/\.[a-z]+$/.test(key);
let add_trailing_slash = true;
const pattern =
id === ''
? /^\/$/
: new RegExp(
`^${decodeURIComponent(id)
.split(/(?:@[a-zA-Z0-9_-]+)?(?:\/|$)/)
.map((segment, i, segments) => {
// special case — /[...rest]/ could contain zero segments
const match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
if (match) {
names.push(match[1]);
types.push(match[2]);
return '(?:/(.*))?';
}
const is_last = i === segments.length - 1;
return (
segment &&
'/' +
segment
.split(/\[(.+?)\]/)
.map((content, i) => {
if (i % 2) {
const [, rest, name, type] = /** @type {RegExpMatchArray} */ (
param_pattern.exec(content)
);
names.push(name);
types.push(type);
return rest ? '(.*?)' : '([^/]+?)';
}
if (is_last && content.includes('.')) add_trailing_slash = false;
return (
content // allow users to specify characters on the file system in an encoded manner
.normalize()
// We use [ and ] to denote parameters, so users must encode these on the file
// system to match against them. We don't decode all characters since others
// can already be epressed and so that '%' can be easily used directly in filenames
.replace(/%5[Bb]/g, '[')
.replace(/%5[Dd]/g, ']')
// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
// They will not be touched by decodeURI so need to be encoded here, so
// that we can match against them.
// We skip '/' since you can't create a file with it on any OS
.replace(/#/g, '%23')
.replace(/\?/g, '%3F')
// escape characters that have special meaning in regex
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
); // TODO handle encoding
})
.join('')
);
})
.join('')}${add_trailing_slash ? '/?' : ''}$`
);
return { pattern, names, types };
}
/**
* @param {RegExpMatchArray} match
* @param {string[]} names
* @param {string[]} types
* @param {Record<string, import('types').ParamMatcher>} matchers
*/
function exec(match, names, types, matchers) {
/** @type {Record<string, string>} */
const params = {};
for (let i = 0; i < names.length; i += 1) {
const name = names[i];
const type = types[i];
const value = match[i + 1] || '';
if (type) {
const matcher = matchers[type];
if (!matcher) throw new Error(`Missing "${type}" param matcher`); // TODO do this ahead of time?
if (!matcher(value)) return;
}
params[name] = value;
}
return params;
}
/**
* @param {import('types').CSRComponentLoader[]} components
* @param {Record<string, [number[], number[], 1?]>} dictionary
* @param {Record<string, (param: string) => boolean>} matchers
* @returns {import('types').CSRRoute[]}
*/
function parse(components, dictionary, matchers) {
const routes = Object.entries(dictionary).map(([id, [a, b, has_shadow]]) => {
const { pattern, names, types } = parse_route_id(id);
return {
id,
/** @param {string} path */
exec: (path) => {
const match = pattern.exec(path);
if (match) return exec(match, names, types, matchers);
},
a: a.map((n) => components[n]),
b: b.map((n) => components[n]),
has_shadow: !!has_shadow
};
});
return routes;
}
const SCROLL_KEY = 'sveltekit:scroll';
const INDEX_KEY = 'sveltekit:index';
const routes = parse(components, dictionary, matchers);
// we import the root layout/error components eagerly, so that
// connectivity errors after initialisation don't nuke the app
const default_layout = components[0]();
const default_error = components[1]();
const root_stuff = {};
// 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
/** @typedef {{ x: number, y: number }} ScrollPosition */
/** @type {Record<number, ScrollPosition>} */
let scroll_positions = {};
try {
scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]);
} catch {
// do nothing
}
/** @param {number} index */
function update_scroll_positions(index) {
scroll_positions[index] = scroll_state();
}
/**
* @param {{
* target: Element;
* session: App.Session;
* base: string;
* trailing_slash: import('types').TrailingSlash;
* }} opts
* @returns {import('./types').Client}
*/
function create_client({ target, session, base, trailing_slash }) {
/** @type {Map<string, import('./types').NavigationResult>} */
const cache = new Map();
/** @type {Array<((href: string) => boolean)>} */
const invalidated = [];
const stores = {
url: notifiable_store({}),
page: notifiable_store({}),
navigating: writable(/** @type {import('types').Navigation | null} */ (null)),
session: writable(session),
updated: create_updated_store()
};
/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
const load_cache = {
id: null,
promise: null
};
const callbacks = {
/** @type {Array<(opts: { from: URL, to: URL | null, cancel: () => void }) => void>} */
before_navigate: [],
/** @type {Array<(opts: { from: URL | null, to: URL }) => void>} */
after_navigate: []
};
/** @type {import('./types').NavigationState} */
let current = {
branch: [],
error: null,
session_id: 0,
stuff: root_stuff,
// @ts-ignore - we need the initial value to be null
url: null
};
let started = false;
let autoscroll = true;
let updating = false;
let session_id = 1;
/** @type {Promise<void> | null} */
let invalidating = null;
/** @type {import('svelte').SvelteComponent} */
let root;
/** @type {App.Session} */
let $session;
let ready = false;
stores.session.subscribe(async (value) => {
$session = value;
if (!ready) return;
session_id += 1;
update(new URL(location.href), [], true);
});
ready = true;
let router_enabled = true;
// 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);
}
let hash_navigating = false;
/** @type {import('types').Page} */
let page;
/** @type {{}} */
let token;
/**
* @param {string | URL} url
* @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts
* @param {string[]} redirect_chain
*/
async function goto(
url,
{ noscroll = false, replaceState = false, keepfocus = false, state = {} },
redirect_chain
) {
if (typeof url === 'string') {
url = new URL(url, get_base_uri(document));
}
if (router_enabled) {
return navigate({
url,
scroll: noscroll ? scroll_state() : null,
keepfocus,
redirect_chain,
details: {
state,
replaceState
},
accepted: () => {},
blocked: () => {}
});
}
await native_navigation(url);
}
/** @param {URL} url */
async function prefetch(url) {
const intent = get_navigation_intent(url);
if (!intent) {
throw new Error('Attempted to prefetch a URL that does not belong to this app');
}
load_cache.promise = load_route(intent, false);
load_cache.id = intent.id;
return load_cache.promise;
}
/**
* Returns `true` if update completes, `false` if it is aborted
* @param {URL} url
* @param {string[]} redirect_chain
* @param {boolean} no_cache
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
* @param {() => void} [callback]
*/
async function update(url, redirect_chain, no_cache, opts, callback) {
const intent = get_navigation_intent(url);
const current_token = (token = {});
let navigation_result = intent && (await load_route(intent, no_cache));
if (
!navigation_result &&
url.origin === location.origin &&
url.pathname === location.pathname
) {
// this could happen in SPA fallback mode if the user navigated to
// `/non-existent-page`. if we fall back to reloading the page, it
// will create an infinite loop. so whereas we normally handle
// unknown routes by going to the server, in this special case
// we render a client-side error page instead
navigation_result = await load_root_error_page({
status: 404,
error: new Error(`Not found: ${url.pathname}`),
url,
routeId: null
});
}
if (!navigation_result) {
await native_navigation(url);
return false; // unnecessary, but TypeScript prefers it this way
}
// abort if user navigated during update
if (token !== current_token) return false;
invalidated.length = 0;
if (navigation_result.redirect) {
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
navigation_result = await load_root_error_page({
status: 500,
error: new Error('Redirect loop'),
url,
routeId: null
});
} else {
if (router_enabled) {
goto(new URL(navigation_result.redirect, url).href, {}, [
...redirect_chain,
url.pathname
]);
} else {
await native_navigation(new URL(navigation_result.redirect, location.href));
}
return false;
}
} else if (navigation_result.props?.page?.status >= 400) {
const updated = await stores.updated.check();
if (updated) {
await native_navigation(url);
}
}
updating = true;
if (opts && opts.details) {
const { details } = opts;
const change = details.replaceState ? 0 : 1;
details.state[INDEX_KEY] = current_history_index += change;
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
}
if (started) {
current = navigation_result.state;
if (navigation_result.props.page) {
navigation_result.props.page.url = url;
}
root.$set(navigation_result.props);
} else {
initialize(navigation_result);
}
// opts must be passed if we're navigating
if (opts) {
const { scroll, keepfocus } = opts;
if (!keepfocus) {
// Reset page selection and focus
// We try to mimic browsers' behaviour as closely as possible by targeting the
// first scrollable region, but unfortunately it's not a perfect match — e.g.
// shift-tabbing won't immediately cycle up from the end of the page on Chromium
// See https://html.spec.whatwg.org/multipage/interaction.html#get-the-focusable-area
const root = document.body;
const tabindex = root.getAttribute('tabindex');
getSelection()?.removeAllRanges();
root.tabIndex = -1;
root.focus({ preventScroll: true });
// restore `tabindex` as to prevent `root` from stealing input from elements
if (tabindex !== null) {
root.setAttribute('tabindex', tabindex);
} else {
root.removeAttribute('tabindex');
}
}
// need to render the DOM before we can scroll to the rendered elements
await tick();
if (autoscroll) {
const deep_linked = url.hash && document.getElementById(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);
}
}
} else {
// in this case we're simply invalidating
await tick();
}
load_cache.promise = null;
load_cache.id = null;
autoscroll = true;
if (navigation_result.props.page) {
page = navigation_result.props.page;
}
const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
router_enabled = leaf_node?.module.router !== false;
if (callback) callback();
updating = false;
}
/** @param {import('./types').NavigationResult} result */
function initialize(result) {
current = result.state;
const style = document.querySelector('style[data-sveltekit]');
if (style) style.remove();
page = result.props.page;
root = new Root({
target,
props: { ...result.props, stores },
hydrate: true
});
if (router_enabled) {
const navigation = { from: null, to: new URL(location.href) };
callbacks.after_navigate.forEach((fn) => fn(navigation));
}
started = true;
}
/**
*
* @param {{
* url: URL;
* params: Record<string, string>;
* stuff: Record<string, any>;
* branch: Array<import('./types').BranchNode | undefined>;
* status: number;
* error: Error | null;
* routeId: string | null;
* }} opts
*/
async function get_navigation_result_from_branch({
url,
params,
stuff,
branch,
status,
error,
routeId
}) {
const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
const redirect = filtered.find((f) => f.loaded?.redirect);
/** @type {import('./types').NavigationResult} */
const result = {
redirect: redirect?.loaded?.redirect,
state: {
url,
params,
branch,
error,
stuff,
session_id
},
props: {
components: filtered.map((node) => node.module.default)
}
};
for (let i = 0; i < filtered.length; i += 1) {
const loaded = filtered[i].loaded;
result.props[`props_${i}`] = loaded ? await loaded.props : null;
}
const page_changed =
!current.url ||
url.href !== current.url.href ||
current.error !== error ||
current.stuff !== stuff;
if (page_changed) {
result.props.page = { error, params, routeId, status, stuff, url };
// TODO remove this for 1.0
/**
* @param {string} property
* @param {string} replacement
*/
const print_error = (property, replacement) => {
Object.defineProperty(result.props.page, property, {
get: () => {
throw new Error(`$page.${property} has been replaced by $page.url.${replacement}`);
}
});
};
print_error('origin', 'origin');
print_error('path', 'pathname');
print_error('query', 'searchParams');
}
const leaf = filtered[filtered.length - 1];
const load_cache = leaf?.loaded?.cache;
if (load_cache) {
const key = url.pathname + url.search; // omit hash
let ready = false;
const clear = () => {
if (cache.get(key) === result) {
cache.delete(key);
}
unsubscribe();
clearTimeout(timeout);
};
const timeout = setTimeout(clear, load_cache.maxage * 1000);
const unsubscribe = stores.session.subscribe(() => {
if (ready) clear();
});
ready = true;
cache.set(key, result);
}
return result;
}
/**
* @param {{
* status?: number;
* error?: Error;
* module: import('types').CSRComponent;
* url: URL;
* params: Record<string, string>;
* stuff: Record<string, any>;
* props?: Record<string, any>;
* routeId: string | null;
* }} options
*/
async function load_node({ status, error, module, url, params, stuff, props, routeId }) {
/** @type {import('./types').BranchNode} */
const node = {
module,
uses: {
params: new Set(),
url: false,
session: false,
stuff: false,
dependencies: new Set()
},
loaded: null,
stuff
};
/** @param dep {string} */
function add_dependency(dep) {
const { href } = new URL(dep, url);
node.uses.dependencies.add(href);
}
if (props) {
// shadow endpoint props means we need to mark this URL as a dependency of itself
node.uses.dependencies.add(url.href);
}
/** @type {Record<string, string>} */
const uses_params = {};
for (const key in params) {
Object.defineProperty(uses_params, key, {
get() {
node.uses.params.add(key);
return params[key];
},
enumerable: true
});
}
const session = $session;
const load_url = new LoadURL(url);
if (module.load) {
/** @type {import('types').LoadEvent} */
const load_input = {
routeId,
params: uses_params,
props: props || {},
get url() {
node.uses.url = true;
return load_url;
},
get session() {
node.uses.session = true;
return session;
},
get stuff() {
node.uses.stuff = true;
return { ...stuff };
},
async fetch(resource, init) {
let requested;
if (typeof resource === 'string') {
requested = resource;
} else {
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
};
}
// we must fixup relative urls so they are resolved from the target page
const normalized = new URL(requested, url).href;
add_dependency(normalized);
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be normalized
return started ? native_fetch(normalized, init) : initial_fetch(requested, init);
},
status: status ?? null,
error: error ?? null
};
if (import.meta.env.DEV) {
// TODO remove this for 1.0
Object.defineProperty(load_input, 'page', {
get: () => {
throw new Error('`page` in `load` functions has been replaced by `url` and `params`');
}
});
}
let loaded;
if (import.meta.env.DEV) {
try {
lock_fetch();
loaded = await module.load.call(null, load_input);
} finally {
unlock_fetch();
}
} else {
loaded = await module.load.call(null, load_input);
}
if (!loaded) {
throw new Error('load function must return a value');
}
node.loaded = normalize(loaded);
if (node.loaded.stuff) node.stuff = node.loaded.stuff;
if (node.loaded.dependencies) {
node.loaded.dependencies.forEach(add_dependency);
}
} else if (props) {
node.loaded = normalize({ props });
}
return node;
}
/**
* @param {import('./types').NavigationIntent} intent
* @param {boolean} no_cache
*/
async function load_route({ id, url, params, route }, no_cache) {
if (load_cache.id === id && load_cache.promise) {
return load_cache.promise;
}
if (!no_cache) {
const cached = cache.get(id);
if (cached) return cached;
}
const { a, b, has_shadow } = route;
const changed = current.url && {
url: id !== current.url.pathname + current.url.search,
params: Object.keys(params).filter((key) => current.params[key] !== params[key]),
session: session_id !== current.session_id
};
/** @type {Array<import('./types').BranchNode | undefined>} */
let branch = [];
/** @type {Record<string, any>} */
let stuff = root_stuff;
let stuff_changed = false;
/** @type {number | undefined} */
let status = 200;
/** @type {Error | null} */
let error = null;
// 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)
a.forEach((loader) => loader().catch(() => {}));
load: for (let i = 0; i < a.length; i += 1) {
/** @type {import('./types').BranchNode | undefined} */
let node;
try {
if (!a[i]) continue;
const module = await a[i]();
const previous = current.branch[i];
const changed_since_last_render =
!previous ||
module !== previous.module ||
(changed.url && previous.uses.url) ||
changed.params.some((param) => previous.uses.params.has(param)) ||
(changed.session && previous.uses.session) ||
Array.from(previous.uses.dependencies).some((dep) => invalidated.some((fn) => fn(dep))) ||
(stuff_changed && previous.uses.stuff);
if (changed_since_last_render) {
/** @type {Record<string, any>} */
let props = {};
const is_shadow_page = has_shadow && i === a.length - 1;
if (is_shadow_page) {
const res = await native_fetch(
`${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
{
headers: {
'x-sveltekit-load': 'true'
}
}
);
if (res.ok) {
const redirect = res.headers.get('x-sveltekit-location');
if (redirect) {
return {
redirect,
props: {},
state: current
};
}
props = res.status === 204 ? {} : await res.json();
} else {
status = res.status;
error = new Error('Failed to load data');
}
}
if (!error) {
node = await load_node({
module,
url,
params,
props,
stuff,
routeId: route.id
});
}
if (node) {
if (is_shadow_page) {
node.uses.url = true;
}
if (node.loaded) {
if (node.loaded.error) {
status = node.loaded.status;
error = node.loaded.error;
}
if (node.loaded.redirect) {
return {
redirect: node.loaded.redirect,
props: {},
state: current
};
}
if (node.loaded.stuff) {
stuff_changed = true;
}
}
}
} else {
node = previous;
}
} catch (e) {
status = 500;
error = coalesce_to_error(e);
}
if (error) {
while (i--) {
if (b[i]) {
let error_loaded;
/** @type {import('./types').BranchNode | undefined} */
let node_loaded;
let j = i;
while (!(node_loaded = branch[j])) {
j -= 1;
}
try {
error_loaded = await load_node({
status,
error,
module: await b[i](),
url,
params,
stuff: node_loaded.stuff,
routeId: route.id
});
if (error_loaded?.loaded?.error) {
continue;
}
if (error_loaded?.loaded?.stuff) {
stuff = {
...stuff,
...error_loaded.loaded.stuff
};
}
branch = branch.slice(0, j + 1).concat(error_loaded);
break load;
} catch (e) {
continue;
}
}
}
return await load_root_error_page({
status,
error,
url,
routeId: route.id
});
} else {
if (node?.loaded?.stuff) {
stuff = {
...stuff,
...node.loaded.stuff
};
}
branch.push(node);
}
}
return await get_navigation_result_from_branch({
url,
params,
stuff,
branch,
status,
error,
routeId: route.id
});
}
/**
* @param {{
* status: number;
* error: Error;
* url: URL;
* routeId: string | null
* }} opts
*/
async function load_root_error_page({ status, error, url, routeId }) {
/** @type {Record<string, string>} */
const params = {}; // error page does not have params
const root_layout = await load_node({
module: await default_layout,
url,
params,
stuff: {},
routeId
});
const root_error = await load_node({
status,
error,
module: await default_error,
url,
params,
stuff: (root_layout && root_layout.loaded && root_layout.loaded.stuff) || {},
routeId
});
return await get_navigation_result_from_branch({
url,
params,
stuff: {
...root_layout?.loaded?.stuff,
...root_error?.loaded?.stuff
},
branch: [root_layout, root_error],
status,
error,
routeId
});
}
/** @param {URL} url */
function get_navigation_intent(url) {
if (url.origin !== location.origin || !url.pathname.startsWith(base)) return;
const path = decodeURI(url.pathname.slice(base.length) || '/');
for (const route of routes) {
const params = route.exec(path);
if (params) {
/** @type {import('./types').NavigationIntent} */
const intent = {
id: url.pathname + url.search,
route,
params,
url
};
return intent;
}
}
}
/**
* @param {{
* url: URL;
* scroll: { x: number, y: number } | null;
* keepfocus: boolean;
* redirect_chain: string[];
* details: {
* replaceState: boolean;
* state: any;
* } | null;
* accepted: () => void;
* blocked: () => void;
* }} opts
*/
async function navigate({ url, scroll, keepfocus, redirect_chain, details, accepted, blocked }) {
const from = current.url;
let should_block = false;
const navigation = {
from,
to: url,
cancel: () => (should_block = true)
};
callbacks.before_navigate.forEach((fn) => fn(navigation));
if (should_block) {
blocked();
return;
}
const pathname = normalize_path(url.pathname, trailing_slash);
const normalized = new URL(url.origin + pathname + url.search + url.hash);
update_scroll_positions(current_history_index);
accepted();
if (started) {
stores.navigating.set({
from: current.url,
to: normalized
});
}
await update(
normalized,
redirect_chain,
false,
{
scroll,
keepfocus,
details
},
() => {
const navigation = { from, to: normalized };
callbacks.after_navigate.forEach((fn) => fn(navigation));
stores.navigating.set(null);
}
);
}
/**
* 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();
});
}
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);
};
});
},
disable_scroll_handling: () => {
if (import.meta.env.DEV && started && !updating) {
throw new Error('Can only disable scroll handling during navigation');
}
if (updating || !started) {
autoscroll = false;
}
},
goto: (href, opts = {}) => goto(href, opts, []),
invalidate: (resource) => {
if (typeof resource === 'function') {
invalidated.push(resource);
} else {
const { href } = new URL(resource, location.href);
invalidated.push((dep) => dep === href);
}
if (!invalidating) {
invalidating = Promise.resolve().then(async () => {
await update(new URL(location.href), [], true);
invalidating = null;
});
}
return invalidating;
},
prefetch: async (href) => {
const url = new URL(href, get_base_uri(document));
await prefetch(url);
},
// TODO rethink this API
prefetch_routes: async (pathnames) => {
const matching = pathnames
? routes.filter((route) => pathnames.some((pathname) => route.exec(pathname)))
: routes;
const promises = matching.map((r) => Promise.all(r.a.map((load) => load())));
await Promise.all(promises);
},
_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;
const navigation = {
from: current.url,
to: null,
cancel: () => (should_block = true)
};
callbacks.before_navigate.forEach((fn) => fn(navigation));
if (should_block) {
e.preventDefault();
e.returnValue = '';
} else {
history.scrollRestoration = 'auto';
}
});
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
update_scroll_positions(current_history_index);
try {
sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions);
} catch {
// do nothing
}
}
});
/** @param {Event} event */
const trigger_prefetch = (event) => {
const a = find_anchor(event);
if (a && a.href && a.hasAttribute('sveltekit:prefetch')) {
prefetch(get_href(a));
}
};
/** @type {NodeJS.Timeout} */
let mousemove_timeout;
/** @param {MouseEvent|TouchEvent} event */
const handle_mousemove = (event) => {
clearTimeout(mousemove_timeout);
mousemove_timeout = setTimeout(() => {
// event.composedPath(), which is used in find_anchor, will be empty if the event is read in a timeout
// add a layer of indirection to address that
event.target?.dispatchEvent(
new CustomEvent('sveltekit:trigger_prefetch', { bubbles: true })
);
}, 20);
};
addEventListener('touchstart', trigger_prefetch);
addEventListener('mousemove', handle_mousemove);
addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);
/** @param {MouseEvent} event */
addEventListener('click', (event) => {
if (!router_enabled) return;
// 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(event);
if (!a) return;
if (!a.href) return;
const is_svg_a_element = a instanceof SVGAElement;
const url = get_href(a);
// Ignore if url does not have origin (e.g. `mailto:`, `tel:`.)
// MEMO: Without this condition, firefox will open mailer twice.
// See: https://github.com/sveltejs/kit/issues/4045
if (!is_svg_a_element && url.origin === 'null') return;
// Ignore if tag has
// 1. 'download' attribute
// 2. 'rel' attribute includes external
const rel = (a.getAttribute('rel') || '').split(/\s+/);
if (
a.hasAttribute('download') ||
rel.includes('external') ||
a.hasAttribute('sveltekit:reload')
) {
return;
}
// Ignore if <a> has a target
if (is_svg_a_element ? a.target.baseVal : a.target) 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 [base, hash] = url.href.split('#');
if (hash !== undefined && base === location.href.split('#')[0]) {
// 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);
stores.page.set({ ...page, url });
stores.page.notify();
return;
}
navigate({
url,
scroll: a.hasAttribute('sveltekit:noscroll') ? scroll_state() : null,
keepfocus: false,
redirect_chain: [],
details: {
state: {},
replaceState: url.href === location.href
},
accepted: () => event.preventDefault(),
blocked: () => event.preventDefault()
});
});
addEventListener('popstate', (event) => {
if (event.state && router_enabled) {
// 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;
navigate({
url: new URL(location.href),
scroll: scroll_positions[event.state[INDEX_KEY]],
keepfocus: false,
redirect_chain: [],
details: null,
accepted: () => {
current_history_index = event.state[INDEX_KEY];
},
blocked: () => {
const delta = current_history_index - event.state[INDEX_KEY];
history.go(delta);
}
});
}
});
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
);
}
});
},
_hydrate: async ({ status, error, nodes, params, routeId }) => {
const url = new URL(location.href);
/** @type {Array<import('./types').BranchNode | undefined>} */
const branch = [];
/** @type {Record<string, any>} */
let stuff = {};
/** @type {import('./types').NavigationResult | undefined} */
let result;
let error_args;
try {
for (let i = 0; i < nodes.length; i += 1) {
const is_leaf = i === nodes.length - 1;
let props;
if (is_leaf) {
const serialized = document.querySelector('script[sveltekit\\:data-type="props"]');
if (serialized) {
props = JSON.parse(/** @type {string} */ (serialized.textContent));
}
}
const node = await load_node({
module: await components[nodes[i]](),
url,
params,
stuff,
status: is_leaf ? status : undefined,
error: is_leaf ? error : undefined,
props,
routeId
});
if (props) {
node.uses.dependencies.add(url.href);
node.uses.url = true;
}
branch.push(node);
if (node && node.loaded) {
if (node.loaded.error) {
if (error) throw node.loaded.error;
error_args = {
status: node.loaded.status,
error: node.loaded.error,
url,
routeId
};
} else if (node.loaded.stuff) {
stuff = {
...stuff,
...node.loaded.stuff
};
}
}
}
result = error_args
? await load_root_error_page(error_args)
: await get_navigation_result_from_branch({
url,
params,
stuff,
branch,
status,
error,
routeId
});
} catch (e) {
if (error) throw e;
result = await load_root_error_page({
status: 500,
error: coalesce_to_error(e),
url,
routeId
});
}
if (result.redirect) {
// this is a real edge case — `load` would need to return
// a redirect but only in the browser
await native_navigation(new URL(result.redirect, location.href));
}
initialize(result);
}
};
}
/**
* @param {{
* paths: {
* assets: string;
* base: string;
* },
* target: Element;
* session: any;
* route: boolean;
* spa: boolean;
* trailing_slash: import('types').TrailingSlash;
* hydrate: {
* status: number;
* error: Error;
* nodes: number[];
* params: Record<string, string>;
* routeId: string | null;
* };
* }} opts
*/
async function start({ paths, target, session, route, spa, trailing_slash, hydrate }) {
const client = create_client({
target,
session,
base: paths.base,
trailing_slash
});
init({ client });
set_paths(paths);
if (hydrate) {
await client._hydrate(hydrate);
}
if (route) {
if (spa) client.goto(location.href, { replaceState: true });
client._start_router();
}
dispatchEvent(new CustomEvent('sveltekit:start'));
}
export { start };