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 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} 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](); 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} */ 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 {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 | 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 | 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; * stuff: Record; * branch: Array; * 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; * 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 }; /** @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} */ 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} */ let branch = []; /** @type {Record} */ 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} */ 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} */ 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 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} */ 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 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; * 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 };