|
import { BROWSER, DEV } from 'esm-env'; |
|
import { onMount, tick } from 'svelte'; |
|
import { |
|
add_data_suffix, |
|
decode_params, |
|
decode_pathname, |
|
strip_hash, |
|
make_trackable, |
|
normalize_path |
|
} from '../../utils/url.js'; |
|
import { |
|
initial_fetch, |
|
lock_fetch, |
|
native_fetch, |
|
subsequent_fetch, |
|
unlock_fetch |
|
} from './fetcher.js'; |
|
import { parse } from './parse.js'; |
|
import * as storage from './session-storage.js'; |
|
import { |
|
find_anchor, |
|
resolve_url, |
|
get_link_info, |
|
get_router_options, |
|
is_external_url, |
|
origin, |
|
scroll_state, |
|
notifiable_store, |
|
create_updated_store |
|
} from './utils.js'; |
|
import { base } from '__sveltekit/paths'; |
|
import * as devalue from 'devalue'; |
|
import { |
|
HISTORY_INDEX, |
|
NAVIGATION_INDEX, |
|
PRELOAD_PRIORITIES, |
|
SCROLL_KEY, |
|
STATES_KEY, |
|
SNAPSHOT_KEY, |
|
PAGE_URL_KEY |
|
} from './constants.js'; |
|
import { validate_page_exports } from '../../utils/exports.js'; |
|
import { compact } from '../../utils/array.js'; |
|
import { HttpError, Redirect, SvelteKitError } from '../control.js'; |
|
import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js'; |
|
import { get_message, get_status } from '../../utils/error.js'; |
|
import { writable } from 'svelte/store'; |
|
|
|
let errored = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const scroll_positions = storage.get(SCROLL_KEY) ?? {}; |
|
|
|
|
|
|
|
|
|
|
|
const snapshots = storage.get(SNAPSHOT_KEY) ?? {}; |
|
|
|
if (DEV && BROWSER) { |
|
let warned = false; |
|
|
|
const current_module_url = import.meta.url.split('?')[0]; |
|
|
|
const warn = () => { |
|
if (warned) return; |
|
|
|
|
|
|
|
let stack = new Error().stack?.split('\n'); |
|
if (!stack) return; |
|
if (!stack[0].includes('https:') && !stack[0].includes('http:')) stack = stack.slice(1); |
|
stack = stack.slice(2); |
|
|
|
if (stack[0]?.includes(current_module_url)) return; |
|
|
|
warned = true; |
|
|
|
console.warn( |
|
"Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use the `pushState` and `replaceState` imports from `$app/navigation` instead." |
|
); |
|
}; |
|
|
|
const push_state = history.pushState; |
|
history.pushState = (...args) => { |
|
warn(); |
|
return push_state.apply(history, args); |
|
}; |
|
|
|
const replace_state = history.replaceState; |
|
history.replaceState = (...args) => { |
|
warn(); |
|
return replace_state.apply(history, args); |
|
}; |
|
} |
|
|
|
export const stores = { |
|
url: notifiable_store({}), |
|
page: notifiable_store({}), |
|
navigating: writable( |
|
(null) |
|
), |
|
updated: create_updated_store() |
|
}; |
|
|
|
|
|
function update_scroll_positions(index) { |
|
scroll_positions[index] = scroll_state(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function clear_onward_history(current_history_index, current_navigation_index) { |
|
|
|
|
|
let i = current_history_index + 1; |
|
while (scroll_positions[i]) { |
|
delete scroll_positions[i]; |
|
i += 1; |
|
} |
|
|
|
i = current_navigation_index + 1; |
|
while (snapshots[i]) { |
|
delete snapshots[i]; |
|
i += 1; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function native_navigation(url) { |
|
location.href = url.href; |
|
return new Promise(() => {}); |
|
} |
|
|
|
function noop() {} |
|
|
|
|
|
let routes; |
|
|
|
let default_layout_loader; |
|
|
|
let default_error_loader; |
|
|
|
let container; |
|
|
|
let target; |
|
|
|
let app; |
|
|
|
|
|
const invalidated = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const components = []; |
|
|
|
|
|
let load_cache = null; |
|
|
|
|
|
const before_navigate_callbacks = []; |
|
|
|
|
|
const on_navigate_callbacks = []; |
|
|
|
|
|
let after_navigate_callbacks = []; |
|
|
|
|
|
let current = { |
|
branch: [], |
|
error: null, |
|
|
|
url: null |
|
}; |
|
|
|
|
|
let hydrated = false; |
|
let started = false; |
|
let autoscroll = true; |
|
let updating = false; |
|
let navigating = false; |
|
let hash_navigating = false; |
|
|
|
let has_navigated = false; |
|
|
|
let force_invalidation = false; |
|
|
|
|
|
let root; |
|
|
|
|
|
let current_history_index; |
|
|
|
|
|
let current_navigation_index; |
|
|
|
|
|
let page; |
|
|
|
|
|
let token; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const preload_tokens = new Set(); |
|
|
|
|
|
let pending_invalidate; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function start(_app, _target, hydrate) { |
|
if (DEV && _target === document.body) { |
|
console.warn( |
|
'Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n %sveltekit.body%\n</div>' |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
if (document.URL !== location.href) { |
|
|
|
location.href = location.href; |
|
} |
|
|
|
app = _app; |
|
routes = parse(_app); |
|
container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement; |
|
target = _target; |
|
|
|
|
|
|
|
default_layout_loader = _app.nodes[0]; |
|
default_error_loader = _app.nodes[1]; |
|
default_layout_loader(); |
|
default_error_loader(); |
|
|
|
current_history_index = history.state?.[HISTORY_INDEX]; |
|
current_navigation_index = history.state?.[NAVIGATION_INDEX]; |
|
|
|
if (!current_history_index) { |
|
|
|
|
|
current_history_index = current_navigation_index = Date.now(); |
|
|
|
|
|
history.replaceState( |
|
{ |
|
...history.state, |
|
[HISTORY_INDEX]: current_history_index, |
|
[NAVIGATION_INDEX]: current_navigation_index |
|
}, |
|
'' |
|
); |
|
} |
|
|
|
|
|
|
|
const scroll = scroll_positions[current_history_index]; |
|
if (scroll) { |
|
history.scrollRestoration = 'manual'; |
|
scrollTo(scroll.x, scroll.y); |
|
} |
|
|
|
if (hydrate) { |
|
await _hydrate(target, hydrate); |
|
} else { |
|
goto(location.href, { replaceState: true }); |
|
} |
|
|
|
_start_router(); |
|
} |
|
|
|
async function _invalidate() { |
|
|
|
|
|
|
|
await (pending_invalidate ||= Promise.resolve()); |
|
if (!pending_invalidate) return; |
|
pending_invalidate = null; |
|
|
|
const intent = get_navigation_intent(current.url, true); |
|
|
|
|
|
|
|
|
|
|
|
load_cache = null; |
|
|
|
const nav_token = (token = {}); |
|
const navigation_result = intent && (await load_route(intent)); |
|
if (!navigation_result || nav_token !== token) return; |
|
|
|
if (navigation_result.type === 'redirect') { |
|
return _goto(new URL(navigation_result.location, current.url).href, {}, 1, nav_token); |
|
} |
|
|
|
if (navigation_result.props.page) { |
|
page = navigation_result.props.page; |
|
} |
|
current = navigation_result.state; |
|
reset_invalidation(); |
|
root.$set(navigation_result.props); |
|
} |
|
|
|
function reset_invalidation() { |
|
invalidated.length = 0; |
|
force_invalidation = false; |
|
} |
|
|
|
|
|
function capture_snapshot(index) { |
|
if (components.some((c) => c?.snapshot)) { |
|
snapshots[index] = components.map((c) => c?.snapshot?.capture()); |
|
} |
|
} |
|
|
|
|
|
function restore_snapshot(index) { |
|
snapshots[index]?.forEach((value, i) => { |
|
components[i]?.snapshot?.restore(value); |
|
}); |
|
} |
|
|
|
function persist_state() { |
|
update_scroll_positions(current_history_index); |
|
storage.set(SCROLL_KEY, scroll_positions); |
|
|
|
capture_snapshot(current_navigation_index); |
|
storage.set(SNAPSHOT_KEY, snapshots); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function _goto(url, options, redirect_count, nav_token) { |
|
return navigate({ |
|
type: 'goto', |
|
url: resolve_url(url), |
|
keepfocus: options.keepFocus, |
|
noscroll: options.noScroll, |
|
replace_state: options.replaceState, |
|
state: options.state, |
|
redirect_count, |
|
nav_token, |
|
accept: () => { |
|
if (options.invalidateAll) { |
|
force_invalidation = true; |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
async function _preload_data(intent) { |
|
|
|
|
|
|
|
|
|
if (intent.id !== load_cache?.id) { |
|
const preload = {}; |
|
preload_tokens.add(preload); |
|
load_cache = { |
|
id: intent.id, |
|
token: preload, |
|
promise: load_route({ ...intent, preload }).then((result) => { |
|
preload_tokens.delete(preload); |
|
if (result.type === 'loaded' && result.state.error) { |
|
|
|
load_cache = null; |
|
} |
|
return result; |
|
}) |
|
}; |
|
} |
|
|
|
return load_cache.promise; |
|
} |
|
|
|
|
|
async function _preload_code(pathname) { |
|
const route = routes.find((route) => route.exec(get_url_path(pathname))); |
|
|
|
if (route) { |
|
await Promise.all([...route.layouts, route.leaf].map((load) => load?.[1]())); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function initialize(result, target, hydrate) { |
|
if (DEV && result.state.error && document.querySelector('vite-error-overlay')) return; |
|
|
|
current = result.state; |
|
|
|
const style = document.querySelector('style[data-sveltekit]'); |
|
if (style) style.remove(); |
|
|
|
page = (result.props.page); |
|
|
|
root = new app.root({ |
|
target, |
|
props: { ...result.props, stores, components }, |
|
hydrate |
|
}); |
|
|
|
restore_snapshot(current_navigation_index); |
|
|
|
|
|
const navigation = { |
|
from: null, |
|
to: { |
|
params: current.params, |
|
route: { id: current.route?.id ?? null }, |
|
url: new URL(location.href) |
|
}, |
|
willUnload: false, |
|
type: 'enter', |
|
complete: Promise.resolve() |
|
}; |
|
|
|
after_navigate_callbacks.forEach((fn) => fn(navigation)); |
|
|
|
started = true; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function get_navigation_result_from_branch({ url, params, branch, status, error, route, form }) { |
|
|
|
let slash = 'never'; |
|
|
|
|
|
|
|
if (base && (url.pathname === base || url.pathname === base + '/')) { |
|
slash = 'always'; |
|
} else { |
|
for (const node of branch) { |
|
if (node?.slash !== undefined) slash = node.slash; |
|
} |
|
} |
|
|
|
url.pathname = normalize_path(url.pathname, slash); |
|
|
|
|
|
url.search = url.search; |
|
|
|
|
|
const result = { |
|
type: 'loaded', |
|
state: { |
|
url, |
|
params, |
|
branch, |
|
error, |
|
route |
|
}, |
|
props: { |
|
|
|
constructors: compact(branch).map((branch_node) => branch_node.node.component), |
|
page |
|
} |
|
}; |
|
|
|
if (form !== undefined) { |
|
result.props.form = form; |
|
} |
|
|
|
let data = {}; |
|
let data_changed = !page; |
|
|
|
let p = 0; |
|
|
|
for (let i = 0; i < Math.max(branch.length, current.branch.length); i += 1) { |
|
const node = branch[i]; |
|
const prev = current.branch[i]; |
|
|
|
if (node?.data !== prev?.data) data_changed = true; |
|
if (!node) continue; |
|
|
|
data = { ...data, ...node.data }; |
|
|
|
|
|
if (data_changed) { |
|
result.props[`data_${p}`] = data; |
|
} |
|
|
|
p += 1; |
|
} |
|
|
|
const page_changed = |
|
!current.url || |
|
url.href !== current.url.href || |
|
current.error !== error || |
|
(form !== undefined && form !== page.form) || |
|
data_changed; |
|
|
|
if (page_changed) { |
|
result.props.page = { |
|
error, |
|
params, |
|
route: { |
|
id: route?.id ?? null |
|
}, |
|
state: {}, |
|
status, |
|
url: new URL(url), |
|
form: form ?? null, |
|
|
|
data: data_changed ? data : page.data |
|
}; |
|
} |
|
|
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function load_node({ loader, parent, url, params, route, server_data_node }) { |
|
|
|
let data = null; |
|
|
|
let is_tracking = true; |
|
|
|
|
|
const uses = { |
|
dependencies: new Set(), |
|
params: new Set(), |
|
parent: false, |
|
route: false, |
|
url: false, |
|
search_params: new Set() |
|
}; |
|
|
|
const node = await loader(); |
|
|
|
if (DEV) { |
|
validate_page_exports(node.universal); |
|
} |
|
|
|
if (node.universal?.load) { |
|
|
|
function depends(...deps) { |
|
for (const dep of deps) { |
|
if (DEV) validate_depends( (route.id), dep); |
|
|
|
const { href } = new URL(dep, url); |
|
uses.dependencies.add(href); |
|
} |
|
} |
|
|
|
|
|
const load_input = { |
|
route: new Proxy(route, { |
|
get: (target, key) => { |
|
if (is_tracking) { |
|
uses.route = true; |
|
} |
|
return target[ (key)]; |
|
} |
|
}), |
|
params: new Proxy(params, { |
|
get: (target, key) => { |
|
if (is_tracking) { |
|
uses.params.add( (key)); |
|
} |
|
return target[ (key)]; |
|
} |
|
}), |
|
data: server_data_node?.data ?? null, |
|
url: make_trackable( |
|
url, |
|
() => { |
|
if (is_tracking) { |
|
uses.url = true; |
|
} |
|
}, |
|
(param) => { |
|
if (is_tracking) { |
|
uses.search_params.add(param); |
|
} |
|
} |
|
), |
|
async fetch(resource, init) { |
|
|
|
let requested; |
|
|
|
if (resource instanceof Request) { |
|
requested = resource.url; |
|
|
|
|
|
|
|
init = { |
|
|
|
|
|
body: |
|
resource.method === 'GET' || resource.method === 'HEAD' |
|
? undefined |
|
: await resource.blob(), |
|
cache: resource.cache, |
|
credentials: resource.credentials, |
|
headers: resource.headers, |
|
integrity: resource.integrity, |
|
keepalive: resource.keepalive, |
|
method: resource.method, |
|
mode: resource.mode, |
|
redirect: resource.redirect, |
|
referrer: resource.referrer, |
|
referrerPolicy: resource.referrerPolicy, |
|
signal: resource.signal, |
|
...init |
|
}; |
|
} else { |
|
requested = resource; |
|
} |
|
|
|
|
|
const resolved = new URL(requested, url); |
|
if (is_tracking) { |
|
depends(resolved.href); |
|
} |
|
|
|
|
|
if (resolved.origin === url.origin) { |
|
requested = resolved.href.slice(url.origin.length); |
|
} |
|
|
|
|
|
return started |
|
? subsequent_fetch(requested, resolved.href, init) |
|
: initial_fetch(requested, init); |
|
}, |
|
setHeaders: () => {}, |
|
depends, |
|
parent() { |
|
if (is_tracking) { |
|
uses.parent = true; |
|
} |
|
return parent(); |
|
}, |
|
untrack(fn) { |
|
is_tracking = false; |
|
try { |
|
return fn(); |
|
} finally { |
|
is_tracking = true; |
|
} |
|
} |
|
}; |
|
|
|
if (DEV) { |
|
try { |
|
lock_fetch(); |
|
data = (await node.universal.load.call(null, load_input)) ?? null; |
|
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { |
|
throw new Error( |
|
`a load function related to route '${route.id}' returned ${ |
|
typeof data !== 'object' |
|
? `a ${typeof data}` |
|
: data instanceof Response |
|
? 'a Response object' |
|
: Array.isArray(data) |
|
? 'an array' |
|
: 'a non-plain object' |
|
}, but must return a plain object at the top level (i.e. \`return {...}\`)` |
|
); |
|
} |
|
} finally { |
|
unlock_fetch(); |
|
} |
|
} else { |
|
data = (await node.universal.load.call(null, load_input)) ?? null; |
|
} |
|
} |
|
|
|
return { |
|
node, |
|
loader, |
|
server: server_data_node, |
|
universal: node.universal?.load ? { type: 'data', data, uses } : null, |
|
data: data ?? server_data_node?.data ?? null, |
|
slash: node.universal?.trailingSlash ?? server_data_node?.slash |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function has_changed( |
|
parent_changed, |
|
route_changed, |
|
url_changed, |
|
search_params_changed, |
|
uses, |
|
params |
|
) { |
|
if (force_invalidation) return true; |
|
|
|
if (!uses) return false; |
|
|
|
if (uses.parent && parent_changed) return true; |
|
if (uses.route && route_changed) return true; |
|
if (uses.url && url_changed) return true; |
|
|
|
for (const tracked_params of uses.search_params) { |
|
if (search_params_changed.has(tracked_params)) return true; |
|
} |
|
|
|
for (const param of uses.params) { |
|
if (params[param] !== current.params[param]) return true; |
|
} |
|
|
|
for (const href of uses.dependencies) { |
|
if (invalidated.some((fn) => fn(new URL(href)))) return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function create_data_node(node, previous) { |
|
if (node?.type === 'data') return node; |
|
if (node?.type === 'skip') return previous ?? null; |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function diff_search_params(old_url, new_url) { |
|
if (!old_url) return new Set(new_url.searchParams.keys()); |
|
|
|
const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]); |
|
|
|
for (const key of changed) { |
|
const old_values = old_url.searchParams.getAll(key); |
|
const new_values = new_url.searchParams.getAll(key); |
|
|
|
if ( |
|
old_values.every((value) => new_values.includes(value)) && |
|
new_values.every((value) => old_values.includes(value)) |
|
) { |
|
changed.delete(key); |
|
} |
|
} |
|
|
|
return changed; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function preload_error({ error, url, route, params }) { |
|
return { |
|
type: 'loaded', |
|
state: { |
|
error, |
|
url, |
|
route, |
|
params, |
|
branch: [] |
|
}, |
|
props: { page, constructors: [] } |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function load_route({ id, invalidating, url, params, route, preload }) { |
|
if (load_cache?.id === id) { |
|
|
|
preload_tokens.delete(load_cache.token); |
|
return load_cache.promise; |
|
} |
|
|
|
const { errors, layouts, leaf } = route; |
|
|
|
const loaders = [...layouts, leaf]; |
|
|
|
|
|
|
|
|
|
errors.forEach((loader) => loader?.().catch(() => {})); |
|
loaders.forEach((loader) => loader?.[1]().catch(() => {})); |
|
|
|
|
|
let server_data = null; |
|
const url_changed = current.url ? id !== current.url.pathname + current.url.search : false; |
|
const route_changed = current.route ? route.id !== current.route.id : false; |
|
const search_params_changed = diff_search_params(current.url, url); |
|
|
|
let parent_invalid = false; |
|
const invalid_server_nodes = loaders.map((loader, i) => { |
|
const previous = current.branch[i]; |
|
|
|
const invalid = |
|
!!loader?.[0] && |
|
(previous?.loader !== loader[1] || |
|
has_changed( |
|
parent_invalid, |
|
route_changed, |
|
url_changed, |
|
search_params_changed, |
|
previous.server?.uses, |
|
params |
|
)); |
|
|
|
if (invalid) { |
|
|
|
parent_invalid = true; |
|
} |
|
|
|
return invalid; |
|
}); |
|
|
|
if (invalid_server_nodes.some(Boolean)) { |
|
try { |
|
server_data = await load_data(url, invalid_server_nodes); |
|
} catch (error) { |
|
const handled_error = await handle_error(error, { url, params, route: { id } }); |
|
|
|
if (preload_tokens.has(preload)) { |
|
return preload_error({ error: handled_error, url, params, route }); |
|
} |
|
|
|
return load_root_error_page({ |
|
status: get_status(error), |
|
error: handled_error, |
|
url, |
|
route |
|
}); |
|
} |
|
|
|
if (server_data.type === 'redirect') { |
|
return server_data; |
|
} |
|
} |
|
|
|
const server_data_nodes = server_data?.nodes; |
|
|
|
let parent_changed = false; |
|
|
|
const branch_promises = loaders.map(async (loader, i) => { |
|
if (!loader) return; |
|
|
|
|
|
const previous = current.branch[i]; |
|
|
|
const server_data_node = server_data_nodes?.[i]; |
|
|
|
|
|
const valid = |
|
(!server_data_node || server_data_node.type === 'skip') && |
|
loader[1] === previous?.loader && |
|
!has_changed( |
|
parent_changed, |
|
route_changed, |
|
url_changed, |
|
search_params_changed, |
|
previous.universal?.uses, |
|
params |
|
); |
|
if (valid) return previous; |
|
|
|
parent_changed = true; |
|
|
|
if (server_data_node?.type === 'error') { |
|
|
|
throw server_data_node; |
|
} |
|
|
|
return load_node({ |
|
loader: loader[1], |
|
url, |
|
params, |
|
route, |
|
parent: async () => { |
|
const data = {}; |
|
for (let j = 0; j < i; j += 1) { |
|
Object.assign(data, (await branch_promises[j])?.data); |
|
} |
|
return data; |
|
}, |
|
server_data_node: create_data_node( |
|
|
|
|
|
server_data_node === undefined && loader[0] ? { type: 'skip' } : server_data_node ?? null, |
|
loader[0] ? previous?.server : undefined |
|
) |
|
}); |
|
}); |
|
|
|
|
|
for (const p of branch_promises) p.catch(() => {}); |
|
|
|
|
|
const branch = []; |
|
|
|
for (let i = 0; i < loaders.length; i += 1) { |
|
if (loaders[i]) { |
|
try { |
|
branch.push(await branch_promises[i]); |
|
} catch (err) { |
|
if (err instanceof Redirect) { |
|
return { |
|
type: 'redirect', |
|
location: err.location |
|
}; |
|
} |
|
|
|
if (preload_tokens.has(preload)) { |
|
return preload_error({ |
|
error: await handle_error(err, { params, url, route: { id: route.id } }), |
|
url, |
|
params, |
|
route |
|
}); |
|
} |
|
|
|
let status = get_status(err); |
|
|
|
let error; |
|
|
|
if (server_data_nodes?.includes( (err))) { |
|
|
|
|
|
status = (err).status ?? status; |
|
error = (err).error; |
|
} else if (err instanceof HttpError) { |
|
error = err.body; |
|
} else { |
|
|
|
const updated = await stores.updated.check(); |
|
if (updated) { |
|
return await native_navigation(url); |
|
} |
|
|
|
error = await handle_error(err, { params, url, route: { id: route.id } }); |
|
} |
|
|
|
const error_load = await load_nearest_error_page(i, branch, errors); |
|
if (error_load) { |
|
return get_navigation_result_from_branch({ |
|
url, |
|
params, |
|
branch: branch.slice(0, error_load.idx).concat(error_load.node), |
|
status, |
|
error, |
|
route |
|
}); |
|
} else { |
|
return await server_fallback(url, { id: route.id }, error, status); |
|
} |
|
} |
|
} else { |
|
|
|
|
|
branch.push(undefined); |
|
} |
|
} |
|
|
|
return get_navigation_result_from_branch({ |
|
url, |
|
params, |
|
branch, |
|
status: 200, |
|
error: null, |
|
route, |
|
|
|
form: invalidating ? undefined : null |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function load_nearest_error_page(i, branch, errors) { |
|
while (i--) { |
|
if (errors[i]) { |
|
let j = i; |
|
while (!branch[j]) j -= 1; |
|
try { |
|
return { |
|
idx: j + 1, |
|
node: { |
|
node: await (errors[i])(), |
|
loader: (errors[i]), |
|
data: {}, |
|
server: null, |
|
universal: null |
|
} |
|
}; |
|
} catch { |
|
continue; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function load_root_error_page({ status, error, url, route }) { |
|
|
|
const params = {}; |
|
|
|
|
|
let server_data_node = null; |
|
|
|
const default_layout_has_server_load = app.server_loads[0] === 0; |
|
|
|
if (default_layout_has_server_load) { |
|
|
|
|
|
try { |
|
const server_data = await load_data(url, [true]); |
|
|
|
if ( |
|
server_data.type !== 'data' || |
|
(server_data.nodes[0] && server_data.nodes[0].type !== 'data') |
|
) { |
|
throw 0; |
|
} |
|
|
|
server_data_node = server_data.nodes[0] ?? null; |
|
} catch { |
|
|
|
|
|
if (url.origin !== origin || url.pathname !== location.pathname || hydrated) { |
|
await native_navigation(url); |
|
} |
|
} |
|
} |
|
|
|
const root_layout = await load_node({ |
|
loader: default_layout_loader, |
|
url, |
|
params, |
|
route, |
|
parent: () => Promise.resolve({}), |
|
server_data_node: create_data_node(server_data_node) |
|
}); |
|
|
|
|
|
const root_error = { |
|
node: await default_error_loader(), |
|
loader: default_error_loader, |
|
universal: null, |
|
server: null, |
|
data: null |
|
}; |
|
|
|
return get_navigation_result_from_branch({ |
|
url, |
|
params, |
|
branch: [root_layout, root_error], |
|
status, |
|
error, |
|
route: null |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function get_navigation_intent(url, invalidating) { |
|
if (!url) return undefined; |
|
if (is_external_url(url, base)) return; |
|
|
|
|
|
let rerouted; |
|
try { |
|
rerouted = app.hooks.reroute({ url: new URL(url) }) ?? url.pathname; |
|
} catch (e) { |
|
if (DEV) { |
|
|
|
console.error(e); |
|
|
|
|
|
debugger; |
|
} |
|
|
|
|
|
return undefined; |
|
} |
|
|
|
const path = get_url_path(rerouted); |
|
|
|
for (const route of routes) { |
|
const params = route.exec(path); |
|
|
|
if (params) { |
|
const id = url.pathname + url.search; |
|
|
|
const intent = { |
|
id, |
|
invalidating, |
|
route, |
|
params: decode_params(params), |
|
url |
|
}; |
|
return intent; |
|
} |
|
} |
|
} |
|
|
|
|
|
function get_url_path(pathname) { |
|
return decode_pathname(pathname.slice(base.length) || '/'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function _before_navigate({ url, type, intent, delta }) { |
|
let should_block = false; |
|
|
|
const nav = create_navigation(current, intent, url, type); |
|
|
|
if (delta !== undefined) { |
|
nav.navigation.delta = delta; |
|
} |
|
|
|
const cancellable = { |
|
...nav.navigation, |
|
cancel: () => { |
|
should_block = true; |
|
nav.reject(new Error('navigation cancelled')); |
|
} |
|
}; |
|
|
|
if (!navigating) { |
|
|
|
before_navigate_callbacks.forEach((fn) => fn(cancellable)); |
|
} |
|
|
|
return should_block ? null : nav; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function navigate({ |
|
type, |
|
url, |
|
popped, |
|
keepfocus, |
|
noscroll, |
|
replace_state, |
|
state = {}, |
|
redirect_count = 0, |
|
nav_token = {}, |
|
accept = noop, |
|
block = noop |
|
}) { |
|
const intent = get_navigation_intent(url, false); |
|
const nav = _before_navigate({ url, type, delta: popped?.delta, intent }); |
|
|
|
if (!nav) { |
|
block(); |
|
return; |
|
} |
|
|
|
|
|
const previous_history_index = current_history_index; |
|
const previous_navigation_index = current_navigation_index; |
|
|
|
accept(); |
|
|
|
navigating = true; |
|
|
|
if (started) { |
|
stores.navigating.set(nav.navigation); |
|
} |
|
|
|
token = nav_token; |
|
let navigation_result = intent && (await load_route(intent)); |
|
|
|
if (!navigation_result) { |
|
if (is_external_url(url, base)) { |
|
return await native_navigation(url); |
|
} |
|
navigation_result = await server_fallback( |
|
url, |
|
{ id: null }, |
|
await handle_error(new SvelteKitError(404, 'Not Found', `Not found: ${url.pathname}`), { |
|
url, |
|
params: {}, |
|
route: { id: null } |
|
}), |
|
404 |
|
); |
|
} |
|
|
|
|
|
|
|
url = intent?.url || url; |
|
|
|
|
|
if (token !== nav_token) { |
|
nav.reject(new Error('navigation aborted')); |
|
return false; |
|
} |
|
|
|
if (navigation_result.type === 'redirect') { |
|
|
|
if (redirect_count >= 20) { |
|
navigation_result = await load_root_error_page({ |
|
status: 500, |
|
error: await handle_error(new Error('Redirect loop'), { |
|
url, |
|
params: {}, |
|
route: { id: null } |
|
}), |
|
url, |
|
route: { id: null } |
|
}); |
|
} else { |
|
_goto(new URL(navigation_result.location, url).href, {}, redirect_count + 1, nav_token); |
|
return false; |
|
} |
|
} else if ( (navigation_result.props.page.status) >= 400) { |
|
const updated = await stores.updated.check(); |
|
if (updated) { |
|
await native_navigation(url); |
|
} |
|
} |
|
|
|
|
|
|
|
reset_invalidation(); |
|
|
|
updating = true; |
|
|
|
update_scroll_positions(previous_history_index); |
|
capture_snapshot(previous_navigation_index); |
|
|
|
|
|
if (navigation_result.props.page.url.pathname !== url.pathname) { |
|
url.pathname = navigation_result.props.page.url.pathname; |
|
} |
|
|
|
state = popped ? popped.state : state; |
|
|
|
if (!popped) { |
|
|
|
const change = replace_state ? 0 : 1; |
|
|
|
const entry = { |
|
[HISTORY_INDEX]: (current_history_index += change), |
|
[NAVIGATION_INDEX]: (current_navigation_index += change), |
|
[STATES_KEY]: state |
|
}; |
|
|
|
const fn = replace_state ? history.replaceState : history.pushState; |
|
fn.call(history, entry, '', url); |
|
|
|
if (!replace_state) { |
|
clear_onward_history(current_history_index, current_navigation_index); |
|
} |
|
} |
|
|
|
|
|
load_cache = null; |
|
|
|
navigation_result.props.page.state = state; |
|
|
|
if (started) { |
|
current = navigation_result.state; |
|
|
|
|
|
if (navigation_result.props.page) { |
|
navigation_result.props.page.url = url; |
|
} |
|
|
|
const after_navigate = ( |
|
await Promise.all( |
|
on_navigate_callbacks.map((fn) => |
|
fn( (nav.navigation)) |
|
) |
|
) |
|
).filter( (value) => typeof value === 'function'); |
|
|
|
if (after_navigate.length > 0) { |
|
function cleanup() { |
|
after_navigate_callbacks = after_navigate_callbacks.filter( |
|
|
|
(fn) => !after_navigate.includes(fn) |
|
); |
|
} |
|
|
|
after_navigate.push(cleanup); |
|
after_navigate_callbacks.push(...after_navigate); |
|
} |
|
|
|
root.$set(navigation_result.props); |
|
has_navigated = true; |
|
} else { |
|
initialize(navigation_result, target, false); |
|
} |
|
|
|
const { activeElement } = document; |
|
|
|
|
|
await tick(); |
|
|
|
|
|
const scroll = popped ? popped.scroll : noscroll ? scroll_state() : null; |
|
|
|
if (autoscroll) { |
|
const deep_linked = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))); |
|
if (scroll) { |
|
scrollTo(scroll.x, scroll.y); |
|
} else if (deep_linked) { |
|
|
|
|
|
|
|
deep_linked.scrollIntoView(); |
|
} else { |
|
scrollTo(0, 0); |
|
} |
|
} |
|
|
|
const changed_focus = |
|
|
|
document.activeElement !== activeElement && |
|
|
|
|
|
document.activeElement !== document.body; |
|
|
|
if (!keepfocus && !changed_focus) { |
|
reset_focus(); |
|
} |
|
|
|
autoscroll = true; |
|
|
|
if (navigation_result.props.page) { |
|
page = navigation_result.props.page; |
|
} |
|
|
|
navigating = false; |
|
|
|
if (type === 'popstate') { |
|
restore_snapshot(current_navigation_index); |
|
} |
|
|
|
nav.fulfil(undefined); |
|
|
|
after_navigate_callbacks.forEach((fn) => |
|
fn( (nav.navigation)) |
|
); |
|
|
|
stores.navigating.set(null); |
|
|
|
updating = false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function server_fallback(url, route, error, status) { |
|
if (url.origin === origin && url.pathname === location.pathname && !hydrated) { |
|
|
|
|
|
return await load_root_error_page({ |
|
status, |
|
error, |
|
url, |
|
route |
|
}); |
|
} |
|
|
|
if (DEV && status !== 404) { |
|
console.error( |
|
'An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)' |
|
); |
|
|
|
debugger; |
|
} |
|
|
|
return await native_navigation(url); |
|
} |
|
|
|
if (import.meta.hot) { |
|
import.meta.hot.on('vite:beforeUpdate', () => { |
|
if (current.error) location.reload(); |
|
}); |
|
} |
|
|
|
function setup_preload() { |
|
|
|
let mousemove_timeout; |
|
|
|
container.addEventListener('mousemove', (event) => { |
|
const target = (event.target); |
|
|
|
clearTimeout(mousemove_timeout); |
|
mousemove_timeout = setTimeout(() => { |
|
preload(target, 2); |
|
}, 20); |
|
}); |
|
|
|
|
|
function tap(event) { |
|
preload( (event.composedPath()[0]), 1); |
|
} |
|
|
|
container.addEventListener('mousedown', tap); |
|
container.addEventListener('touchstart', tap, { passive: true }); |
|
|
|
const observer = new IntersectionObserver( |
|
(entries) => { |
|
for (const entry of entries) { |
|
if (entry.isIntersecting) { |
|
_preload_code( (entry.target).href); |
|
observer.unobserve(entry.target); |
|
} |
|
} |
|
}, |
|
{ threshold: 0 } |
|
); |
|
|
|
|
|
|
|
|
|
|
|
function preload(element, priority) { |
|
const a = find_anchor(element, container); |
|
if (!a) return; |
|
|
|
const { url, external, download } = get_link_info(a, base); |
|
if (external || download) return; |
|
|
|
const options = get_router_options(a); |
|
|
|
if (!options.reload) { |
|
if (priority <= options.preload_data) { |
|
const intent = get_navigation_intent(url, false); |
|
if (intent) { |
|
if (DEV) { |
|
_preload_data(intent).then((result) => { |
|
if (result.type === 'loaded' && result.state.error) { |
|
console.warn( |
|
`Preloading data for ${intent.url.pathname} failed with the following error: ${result.state.error.message}\n` + |
|
'If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. ' + |
|
'This route was preloaded due to a data-sveltekit-preload-data attribute. ' + |
|
'See https://kit.svelte.dev/docs/link-options for more info' |
|
); |
|
} |
|
}); |
|
} else { |
|
_preload_data(intent); |
|
} |
|
} |
|
} else if (priority <= options.preload_code) { |
|
_preload_code( (url).pathname); |
|
} |
|
} |
|
} |
|
|
|
function after_navigate() { |
|
observer.disconnect(); |
|
|
|
for (const a of container.querySelectorAll('a')) { |
|
const { url, external, download } = get_link_info(a, base); |
|
if (external || download) continue; |
|
|
|
const options = get_router_options(a); |
|
if (options.reload) continue; |
|
|
|
if (options.preload_code === PRELOAD_PRIORITIES.viewport) { |
|
observer.observe(a); |
|
} |
|
|
|
if (options.preload_code === PRELOAD_PRIORITIES.eager) { |
|
_preload_code( (url).pathname); |
|
} |
|
} |
|
} |
|
|
|
after_navigate_callbacks.push(after_navigate); |
|
after_navigate(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function handle_error(error, event) { |
|
if (error instanceof HttpError) { |
|
return error.body; |
|
} |
|
|
|
if (DEV) { |
|
errored = true; |
|
console.warn('The next HMR update will cause the page to reload'); |
|
} |
|
|
|
const status = get_status(error); |
|
const message = get_message(error); |
|
|
|
return ( |
|
app.hooks.handleError({ error, event, status, message }) ?? ({ message }) |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function add_navigation_callback(callbacks, callback) { |
|
onMount(() => { |
|
callbacks.push(callback); |
|
|
|
return () => { |
|
const i = callbacks.indexOf(callback); |
|
callbacks.splice(i, 1); |
|
}; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function afterNavigate(callback) { |
|
add_navigation_callback(after_navigate_callbacks, callback); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function beforeNavigate(callback) { |
|
add_navigation_callback(before_navigate_callbacks, callback); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function onNavigate(callback) { |
|
add_navigation_callback(on_navigate_callbacks, callback); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function disableScrollHandling() { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call disableScrollHandling() on the server'); |
|
} |
|
|
|
if (DEV && started && !updating) { |
|
throw new Error('Can only disable scroll handling during navigation'); |
|
} |
|
|
|
if (updating || !started) { |
|
autoscroll = false; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function goto(url, opts = {}) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call goto(...) on the server'); |
|
} |
|
|
|
url = resolve_url(url); |
|
|
|
if (url.origin !== origin) { |
|
return Promise.reject( |
|
new Error( |
|
DEV |
|
? `Cannot use \`goto\` with an external URL. Use \`window.location = "${url}"\` instead` |
|
: 'goto: invalid URL' |
|
) |
|
); |
|
} |
|
|
|
return _goto(url, opts, 0); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function invalidate(resource) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call invalidate(...) on the server'); |
|
} |
|
|
|
if (typeof resource === 'function') { |
|
invalidated.push(resource); |
|
} else { |
|
const { href } = new URL(resource, location.href); |
|
invalidated.push((url) => url.href === href); |
|
} |
|
|
|
return _invalidate(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function invalidateAll() { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call invalidateAll() on the server'); |
|
} |
|
|
|
force_invalidation = true; |
|
return _invalidate(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function preloadData(href) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call preloadData(...) on the server'); |
|
} |
|
|
|
const url = resolve_url(href); |
|
const intent = get_navigation_intent(url, false); |
|
|
|
if (!intent) { |
|
throw new Error(`Attempted to preload a URL that does not belong to this app: ${url}`); |
|
} |
|
|
|
const result = await _preload_data(intent); |
|
if (result.type === 'redirect') { |
|
return { |
|
type: result.type, |
|
location: result.location |
|
}; |
|
} |
|
|
|
const { status, data } = result.props.page ?? page; |
|
return { type: result.type, status, data }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function preloadCode(pathname) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call preloadCode(...) on the server'); |
|
} |
|
|
|
if (DEV) { |
|
if (!pathname.startsWith(base)) { |
|
throw new Error( |
|
`pathnames passed to preloadCode must start with \`paths.base\` (i.e. "${base}${pathname}" rather than "${pathname}")` |
|
); |
|
} |
|
|
|
if (!routes.find((route) => route.exec(get_url_path(pathname)))) { |
|
throw new Error(`'${pathname}' did not match any routes`); |
|
} |
|
} |
|
|
|
return _preload_code(pathname); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function pushState(url, state) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call pushState(...) on the server'); |
|
} |
|
|
|
if (DEV) { |
|
try { |
|
|
|
devalue.stringify(state); |
|
} catch (error) { |
|
|
|
throw new Error(`Could not serialize state${error.path}`); |
|
} |
|
} |
|
|
|
update_scroll_positions(current_history_index); |
|
|
|
const opts = { |
|
[HISTORY_INDEX]: (current_history_index += 1), |
|
[NAVIGATION_INDEX]: current_navigation_index, |
|
[PAGE_URL_KEY]: page.url.href, |
|
[STATES_KEY]: state |
|
}; |
|
|
|
history.pushState(opts, '', resolve_url(url)); |
|
has_navigated = true; |
|
|
|
page = { ...page, state }; |
|
root.$set({ page }); |
|
|
|
clear_onward_history(current_history_index, current_navigation_index); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function replaceState(url, state) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call replaceState(...) on the server'); |
|
} |
|
|
|
if (DEV) { |
|
try { |
|
|
|
devalue.stringify(state); |
|
} catch (error) { |
|
|
|
throw new Error(`Could not serialize state${error.path}`); |
|
} |
|
} |
|
|
|
const opts = { |
|
[HISTORY_INDEX]: current_history_index, |
|
[NAVIGATION_INDEX]: current_navigation_index, |
|
[PAGE_URL_KEY]: page.url.href, |
|
[STATES_KEY]: state |
|
}; |
|
|
|
history.replaceState(opts, '', resolve_url(url)); |
|
|
|
page = { ...page, state }; |
|
root.$set({ page }); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function applyAction(result) { |
|
if (!BROWSER) { |
|
throw new Error('Cannot call applyAction(...) on the server'); |
|
} |
|
|
|
if (result.type === 'error') { |
|
const url = new URL(location.href); |
|
|
|
const { branch, route } = current; |
|
if (!route) return; |
|
|
|
const error_load = await load_nearest_error_page(current.branch.length, branch, route.errors); |
|
if (error_load) { |
|
const navigation_result = get_navigation_result_from_branch({ |
|
url, |
|
params: current.params, |
|
branch: branch.slice(0, error_load.idx).concat(error_load.node), |
|
status: result.status ?? 500, |
|
error: result.error, |
|
route |
|
}); |
|
|
|
current = navigation_result.state; |
|
|
|
root.$set(navigation_result.props); |
|
|
|
tick().then(reset_focus); |
|
} |
|
} else if (result.type === 'redirect') { |
|
_goto(result.location, { invalidateAll: true }, 0); |
|
} else { |
|
|
|
root.$set({ |
|
|
|
|
|
form: null, |
|
page: { ...page, form: result.data, status: result.status } |
|
}); |
|
|
|
|
|
await tick(); |
|
root.$set({ form: result.data }); |
|
|
|
if (result.type === 'success') { |
|
reset_focus(); |
|
} |
|
} |
|
} |
|
|
|
function _start_router() { |
|
history.scrollRestoration = 'manual'; |
|
|
|
|
|
|
|
|
|
|
|
addEventListener('beforeunload', (e) => { |
|
let should_block = false; |
|
|
|
persist_state(); |
|
|
|
if (!navigating) { |
|
const nav = create_navigation(current, undefined, null, 'leave'); |
|
|
|
|
|
|
|
|
|
const navigation = { |
|
...nav.navigation, |
|
cancel: () => { |
|
should_block = true; |
|
nav.reject(new Error('navigation cancelled')); |
|
} |
|
}; |
|
|
|
before_navigate_callbacks.forEach((fn) => fn(navigation)); |
|
} |
|
|
|
if (should_block) { |
|
e.preventDefault(); |
|
e.returnValue = ''; |
|
} else { |
|
history.scrollRestoration = 'auto'; |
|
} |
|
}); |
|
|
|
addEventListener('visibilitychange', () => { |
|
if (document.visibilityState === 'hidden') { |
|
persist_state(); |
|
} |
|
}); |
|
|
|
|
|
if (!navigator.connection?.saveData) { |
|
setup_preload(); |
|
} |
|
|
|
|
|
container.addEventListener('click', async (event) => { |
|
|
|
|
|
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.composedPath()[0]), container); |
|
if (!a) return; |
|
|
|
const { url, external, target, download } = get_link_info(a, base); |
|
if (!url) return; |
|
|
|
|
|
if (target === '_parent' || target === '_top') { |
|
if (window.parent !== window) return; |
|
} else if (target && target !== '_self') { |
|
return; |
|
} |
|
|
|
const options = get_router_options(a); |
|
const is_svg_a_element = a instanceof SVGAElement; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
!is_svg_a_element && |
|
url.protocol !== location.protocol && |
|
!(url.protocol === 'https:' || url.protocol === 'http:') |
|
) |
|
return; |
|
|
|
if (download) return; |
|
|
|
|
|
if (external || options.reload) { |
|
if (_before_navigate({ url, type: 'link' })) { |
|
|
|
|
|
navigating = true; |
|
} else { |
|
event.preventDefault(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
|
|
|
|
|
|
const [nonhash, hash] = url.href.split('#'); |
|
if (hash !== undefined && nonhash === strip_hash(location)) { |
|
|
|
|
|
|
|
|
|
const [, current_hash] = current.url.href.split('#'); |
|
if (current_hash === hash) { |
|
event.preventDefault(); |
|
|
|
|
|
|
|
|
|
if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) { |
|
window.scrollTo({ top: 0 }); |
|
} else { |
|
a.ownerDocument.getElementById(hash)?.scrollIntoView(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
|
|
hash_navigating = true; |
|
|
|
update_scroll_positions(current_history_index); |
|
|
|
update_url(url); |
|
|
|
if (!options.replace_state) return; |
|
|
|
|
|
hash_navigating = false; |
|
} |
|
|
|
event.preventDefault(); |
|
|
|
|
|
|
|
await new Promise((fulfil) => { |
|
requestAnimationFrame(() => { |
|
setTimeout(fulfil, 0); |
|
}); |
|
|
|
setTimeout(fulfil, 100); |
|
}); |
|
|
|
navigate({ |
|
type: 'link', |
|
url, |
|
keepfocus: options.keepfocus, |
|
noscroll: options.noscroll, |
|
replace_state: options.replace_state ?? url.href === location.href |
|
}); |
|
}); |
|
|
|
container.addEventListener('submit', (event) => { |
|
if (event.defaultPrevented) return; |
|
|
|
const form = ( |
|
HTMLFormElement.prototype.cloneNode.call(event.target) |
|
); |
|
|
|
const submitter = (event.submitter); |
|
|
|
const method = submitter?.formMethod || form.method; |
|
|
|
if (method !== 'get') return; |
|
|
|
const url = new URL( |
|
(submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action |
|
); |
|
|
|
if (is_external_url(url, base)) return; |
|
|
|
const event_form = (event.target); |
|
|
|
const options = get_router_options(event_form); |
|
if (options.reload) return; |
|
|
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
|
|
const data = new FormData(event_form); |
|
|
|
const submitter_name = submitter?.getAttribute('name'); |
|
if (submitter_name) { |
|
data.append(submitter_name, submitter?.getAttribute('value') ?? ''); |
|
} |
|
|
|
|
|
url.search = new URLSearchParams(data).toString(); |
|
|
|
navigate({ |
|
type: 'form', |
|
url, |
|
keepfocus: options.keepfocus, |
|
noscroll: options.noscroll, |
|
replace_state: options.replace_state ?? url.href === location.href |
|
}); |
|
}); |
|
|
|
addEventListener('popstate', async (event) => { |
|
if (event.state?.[HISTORY_INDEX]) { |
|
const history_index = event.state[HISTORY_INDEX]; |
|
token = {}; |
|
|
|
|
|
|
|
if (history_index === current_history_index) return; |
|
|
|
const scroll = scroll_positions[history_index]; |
|
const state = event.state[STATES_KEY] ?? {}; |
|
const url = new URL(event.state[PAGE_URL_KEY] ?? location.href); |
|
const navigation_index = event.state[NAVIGATION_INDEX]; |
|
const is_hash_change = strip_hash(location) === strip_hash(current.url); |
|
const shallow = |
|
navigation_index === current_navigation_index && (has_navigated || is_hash_change); |
|
|
|
if (shallow) { |
|
|
|
|
|
|
|
|
|
update_url(url); |
|
|
|
scroll_positions[current_history_index] = scroll_state(); |
|
if (scroll) scrollTo(scroll.x, scroll.y); |
|
|
|
if (state !== page.state) { |
|
page = { ...page, state }; |
|
root.$set({ page }); |
|
} |
|
|
|
current_history_index = history_index; |
|
return; |
|
} |
|
|
|
const delta = history_index - current_history_index; |
|
|
|
await navigate({ |
|
type: 'popstate', |
|
url, |
|
popped: { |
|
state, |
|
scroll, |
|
delta |
|
}, |
|
accept: () => { |
|
current_history_index = history_index; |
|
current_navigation_index = navigation_index; |
|
}, |
|
block: () => { |
|
history.go(-delta); |
|
}, |
|
nav_token: token |
|
}); |
|
} else { |
|
|
|
|
|
|
|
if (!hash_navigating) { |
|
const url = new URL(location.href); |
|
update_url(url); |
|
} |
|
} |
|
}); |
|
|
|
addEventListener('hashchange', () => { |
|
|
|
|
|
if (hash_navigating) { |
|
hash_navigating = false; |
|
history.replaceState( |
|
{ |
|
...history.state, |
|
[HISTORY_INDEX]: ++current_history_index, |
|
[NAVIGATION_INDEX]: current_navigation_index |
|
}, |
|
'', |
|
location.href |
|
); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
for (const link of document.querySelectorAll('link')) { |
|
if (link.rel === 'icon') link.href = link.href; |
|
} |
|
|
|
addEventListener('pageshow', (event) => { |
|
|
|
|
|
|
|
|
|
if (event.persisted) { |
|
stores.navigating.set(null); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
function update_url(url) { |
|
current.url = url; |
|
stores.page.set({ ...page, url }); |
|
stores.page.notify(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function _hydrate( |
|
target, |
|
{ status = 200, error, node_ids, params, route, data: server_data_nodes, form } |
|
) { |
|
hydrated = true; |
|
|
|
const url = new URL(location.href); |
|
|
|
if (!__SVELTEKIT_EMBEDDED__) { |
|
|
|
|
|
({ params = {}, route = { id: null } } = get_navigation_intent(url, false) || {}); |
|
} |
|
|
|
|
|
let result; |
|
|
|
try { |
|
const branch_promises = node_ids.map(async (n, i) => { |
|
const server_data_node = server_data_nodes[i]; |
|
|
|
if (server_data_node?.uses) { |
|
server_data_node.uses = deserialize_uses(server_data_node.uses); |
|
} |
|
|
|
return load_node({ |
|
loader: app.nodes[n], |
|
url, |
|
params, |
|
route, |
|
parent: async () => { |
|
const data = {}; |
|
for (let j = 0; j < i; j += 1) { |
|
Object.assign(data, (await branch_promises[j]).data); |
|
} |
|
return data; |
|
}, |
|
server_data_node: create_data_node(server_data_node) |
|
}); |
|
}); |
|
|
|
|
|
const branch = await Promise.all(branch_promises); |
|
|
|
const parsed_route = routes.find(({ id }) => id === route.id); |
|
|
|
|
|
|
|
if (parsed_route) { |
|
const layouts = parsed_route.layouts; |
|
for (let i = 0; i < layouts.length; i++) { |
|
if (!layouts[i]) { |
|
branch.splice(i, 0, undefined); |
|
} |
|
} |
|
} |
|
|
|
result = get_navigation_result_from_branch({ |
|
url, |
|
params, |
|
branch, |
|
status, |
|
error, |
|
form, |
|
route: parsed_route ?? null |
|
}); |
|
} catch (error) { |
|
if (error instanceof Redirect) { |
|
|
|
|
|
await native_navigation(new URL(error.location, location.href)); |
|
return; |
|
} |
|
|
|
result = await load_root_error_page({ |
|
status: get_status(error), |
|
error: await handle_error(error, { url, params, route }), |
|
url, |
|
route |
|
}); |
|
} |
|
|
|
if (result.props.page) { |
|
result.props.page.state = {}; |
|
} |
|
|
|
initialize(result, target, true); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
async function load_data(url, invalid) { |
|
const data_url = new URL(url); |
|
data_url.pathname = add_data_suffix(url.pathname); |
|
if (url.pathname.endsWith('/')) { |
|
data_url.searchParams.append(TRAILING_SLASH_PARAM, '1'); |
|
} |
|
if (DEV && url.searchParams.has(INVALIDATED_PARAM)) { |
|
throw new Error(`Cannot used reserved query parameter "${INVALIDATED_PARAM}"`); |
|
} |
|
data_url.searchParams.append(INVALIDATED_PARAM, invalid.map((i) => (i ? '1' : '0')).join('')); |
|
|
|
const res = await native_fetch(data_url.href); |
|
|
|
if (!res.ok) { |
|
|
|
|
|
|
|
|
|
|
|
let message; |
|
if (res.headers.get('content-type')?.includes('application/json')) { |
|
message = await res.json(); |
|
} else if (res.status === 404) { |
|
message = 'Not Found'; |
|
} else if (res.status === 500) { |
|
message = 'Internal Error'; |
|
} |
|
throw new HttpError(res.status, message); |
|
} |
|
|
|
|
|
|
|
return new Promise(async (resolve) => { |
|
|
|
|
|
|
|
|
|
const deferreds = new Map(); |
|
const reader = (res.body).getReader(); |
|
const decoder = new TextDecoder(); |
|
|
|
|
|
|
|
|
|
function deserialize(data) { |
|
return devalue.unflatten(data, { |
|
Promise: (id) => { |
|
return new Promise((fulfil, reject) => { |
|
deferreds.set(id, { fulfil, reject }); |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
let text = ''; |
|
|
|
while (true) { |
|
|
|
const { done, value } = await reader.read(); |
|
if (done && !text) break; |
|
|
|
text += !value && text ? '\n' : decoder.decode(value, { stream: true }); |
|
|
|
while (true) { |
|
const split = text.indexOf('\n'); |
|
if (split === -1) { |
|
break; |
|
} |
|
|
|
const node = JSON.parse(text.slice(0, split)); |
|
text = text.slice(split + 1); |
|
|
|
if (node.type === 'redirect') { |
|
return resolve(node); |
|
} |
|
|
|
if (node.type === 'data') { |
|
|
|
node.nodes?.forEach(( node) => { |
|
if (node?.type === 'data') { |
|
node.uses = deserialize_uses(node.uses); |
|
node.data = deserialize(node.data); |
|
} |
|
}); |
|
|
|
resolve(node); |
|
} else if (node.type === 'chunk') { |
|
|
|
const { id, data, error } = node; |
|
const deferred = (deferreds.get(id)); |
|
deferreds.delete(id); |
|
|
|
if (error) { |
|
deferred.reject(deserialize(error)); |
|
} else { |
|
deferred.fulfil(deserialize(data)); |
|
} |
|
} |
|
} |
|
} |
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function deserialize_uses(uses) { |
|
return { |
|
dependencies: new Set(uses?.dependencies ?? []), |
|
params: new Set(uses?.params ?? []), |
|
parent: !!uses?.parent, |
|
route: !!uses?.route, |
|
url: !!uses?.url, |
|
search_params: new Set(uses?.search_params ?? []) |
|
}; |
|
} |
|
|
|
function reset_focus() { |
|
const autofocus = document.querySelector('[autofocus]'); |
|
if (autofocus) { |
|
|
|
autofocus.focus(); |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
const root = document.body; |
|
const tabindex = root.getAttribute('tabindex'); |
|
|
|
root.tabIndex = -1; |
|
|
|
root.focus({ preventScroll: true, focusVisible: false }); |
|
|
|
|
|
if (tabindex !== null) { |
|
root.setAttribute('tabindex', tabindex); |
|
} else { |
|
root.removeAttribute('tabindex'); |
|
} |
|
|
|
|
|
|
|
const selection = getSelection(); |
|
|
|
if (selection && selection.type !== 'None') { |
|
|
|
const ranges = []; |
|
|
|
for (let i = 0; i < selection.rangeCount; i += 1) { |
|
ranges.push(selection.getRangeAt(i)); |
|
} |
|
|
|
setTimeout(() => { |
|
if (selection.rangeCount !== ranges.length) return; |
|
|
|
for (let i = 0; i < selection.rangeCount; i += 1) { |
|
const a = ranges[i]; |
|
const b = selection.getRangeAt(i); |
|
|
|
|
|
|
|
if ( |
|
a.commonAncestorContainer !== b.commonAncestorContainer || |
|
a.startContainer !== b.startContainer || |
|
a.endContainer !== b.endContainer || |
|
a.startOffset !== b.startOffset || |
|
a.endOffset !== b.endOffset |
|
) { |
|
return; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
selection.removeAllRanges(); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function create_navigation(current, intent, url, type) { |
|
|
|
let fulfil; |
|
|
|
|
|
let reject; |
|
|
|
const complete = new Promise((f, r) => { |
|
fulfil = f; |
|
reject = r; |
|
}); |
|
|
|
|
|
complete.catch(() => {}); |
|
|
|
|
|
const navigation = { |
|
from: { |
|
params: current.params, |
|
route: { id: current.route?.id ?? null }, |
|
url: current.url |
|
}, |
|
to: url && { |
|
params: intent?.params ?? null, |
|
route: { id: intent?.route?.id ?? null }, |
|
url |
|
}, |
|
willUnload: !intent, |
|
type, |
|
complete |
|
}; |
|
|
|
return { |
|
navigation, |
|
|
|
fulfil, |
|
|
|
reject |
|
}; |
|
} |
|
|
|
if (DEV) { |
|
|
|
const console_warn = console.warn; |
|
console.warn = function warn(...args) { |
|
if ( |
|
args.length === 1 && |
|
/<(Layout|Page|Error)(_[\w$]+)?> was created (with unknown|without expected) prop '(data|form)'/.test( |
|
args[0] |
|
) |
|
) { |
|
return; |
|
} |
|
console_warn(...args); |
|
}; |
|
|
|
if (import.meta.hot) { |
|
import.meta.hot.on('vite:beforeUpdate', () => { |
|
if (errored) { |
|
location.reload(); |
|
} |
|
}); |
|
} |
|
} |
|
|