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) { 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) { return { status: 500, error: new Error( '"redirect" property returned from load() must be accompanied by a 3xx status code' ) }; } if (typeof loaded.redirect !== 'string') { return { status: 500, error: new Error('"redirect" property returned from load() must be a 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' && /\/[^./]+$/.test(path)) { return path + '/'; } return path; } /** * 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); } /** @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 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 }; } /** * @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 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('/') .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 .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} matchers */ function exec(match, names, types, matchers) { /** @type {Record} */ 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} dictionary * @param {Record 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](); // 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} */ 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} */ const cache = new Map(); /** @type {Set} */ const invalidated = new Set(); 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 | 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 = { // @ts-ignore - we need the initial value to be null url: null, session_id: 0, branch: [] }; let started = false; let autoscroll = true; let updating = false; let session_id = 1; /** @type {Promise | 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; /** Keeps tracks of multiple navigations caused by redirects during rendering */ let navigating = 0; 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] ?? 0; if (current_history_index === 0) { // create initial history entry, so we can return here history.replaceState({ ...history.state, [INDEX_KEY]: 0 }, '', 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) scrollTo(scroll.x, scroll.y); let hash_navigating = false; /** @type {import('types').Page} */ let page; /** @type {{}} */ let token; /** @type {{}} */ let navigating_token; /** * @param {string} href * @param {{ noscroll?: boolean; replaceState?: boolean; keepfocus?: boolean; state?: any }} opts * @param {string[]} redirect_chain */ async function goto( href, { noscroll = false, replaceState = false, keepfocus = false, state = {} }, redirect_chain ) { const url = new URL(href, 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; } /** * @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] */ async function update(url, redirect_chain, no_cache, opts) { const intent = get_navigation_intent(url); const current_token = (token = {}); let navigation_result = intent && (await load_route(intent, no_cache)); if (!navigation_result && 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; // unnecessary, but TypeScript prefers it this way } // abort if user navigated during update if (token !== current_token) return; invalidated.clear(); 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; } } 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; 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(); // 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; updating = false; 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; } /** @param {import('./types').NavigationResult} result */ function initialize(result) { current = result.state; const style = document.querySelector('style[data-svelte]'); if (style) style.remove(); page = result.props.page; root = new Root({ target, props: { ...result.props, stores }, hydrate: true }); started = true; if (router_enabled) { const navigation = { from: null, to: new URL(location.href) }; callbacks.after_navigate.forEach((fn) => fn(navigation)); } } /** * * @param {{ * url: URL; * params: Record; * stuff: Record; * branch: Array; * status: number; * error?: Error; * 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, 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; } if (!current.url || url.href !== current.url.href) { 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 maxage = leaf.loaded && leaf.loaded.maxage; if (maxage) { 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, 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; * stuff: Record; * props?: Record; * 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 }; 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} */ 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; if (module.load) { /** @type {import('types').LoadInput | import('types').ErrorLoadInput} */ const load_input = { routeId, params: uses_params, props: props || {}, get url() { node.uses.url = true; return url; }, get session() { node.uses.session = true; return session; }, get stuff() { node.uses.stuff = true; return { ...stuff }; }, fetch(resource, info) { const requested = typeof resource === 'string' ? resource : resource.url; const { href } = new URL(requested, url); node.uses.dependencies.add(href); return started ? fetch(resource, info) : initial_fetch(resource, info); } }; 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`'); } }); } if (error) { /** @type {import('types').ErrorLoadInput} */ (load_input).status = status; /** @type {import('types').ErrorLoadInput} */ (load_input).error = error; } const 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; } 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} */ let branch = []; /** @type {Record} */ let stuff = {}; let stuff_changed = false; /** @type {number | undefined} */ let status = 200; /** @type {Error | undefined} */ let error; // preload modules a.forEach((loader) => loader()); 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.has(dep)) || (stuff_changed && previous.uses.stuff); if (changed_since_last_render) { /** @type {Record} */ let props = {}; const is_shadow_page = has_shadow && i === a.length - 1; if (is_shadow_page) { const res = await 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) { // TODO remove for 1.0 // @ts-expect-error if (node.loaded.fallthrough) { throw new Error( 'fallthrough is no longer supported. Use matchers instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation' ); } 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} */ 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(); navigating++; const current_navigating_token = (navigating_token = {}); if (started) { stores.navigating.set({ from: current.url, to: normalized }); } await update(normalized, redirect_chain, false, { scroll, keepfocus, details }); navigating--; // navigation was aborted if (navigating_token !== current_navigating_token) return; if (!navigating) { 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(() => {}); } 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) => { const { href } = new URL(resource, location.href); invalidated.add(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')) { return; } // Ignore if has a target if (is_svg_a_element ? a.target.baseVal : a.target) return; if (url.href === location.href) { if (!location.hash) 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 [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: false }, 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} */ const branch = []; /** @type {Record} */ 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 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: Array>; * params: Record; * 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 };