371 lines
8.8 KiB
JavaScript
371 lines
8.8 KiB
JavaScript
import { BROWSER, DEV } from 'esm-env';
|
|
import { writable } from 'svelte/store';
|
|
import { assets } from '__sveltekit/paths';
|
|
import { version } from '__sveltekit/environment';
|
|
import { PRELOAD_PRIORITIES } from './constants.js';
|
|
|
|
/* global __SVELTEKIT_APP_VERSION_FILE__, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__ */
|
|
|
|
export const origin = BROWSER ? location.origin : '';
|
|
|
|
/** @param {string | URL} url */
|
|
export function resolve_url(url) {
|
|
if (url instanceof URL) return url;
|
|
|
|
let baseURI = document.baseURI;
|
|
|
|
if (!baseURI) {
|
|
const baseTags = document.getElementsByTagName('base');
|
|
baseURI = baseTags.length ? baseTags[0].href : document.URL;
|
|
}
|
|
|
|
return new URL(url, baseURI);
|
|
}
|
|
|
|
export function scroll_state() {
|
|
return {
|
|
x: pageXOffset,
|
|
y: pageYOffset
|
|
};
|
|
}
|
|
|
|
const warned = new WeakSet();
|
|
|
|
/** @typedef {keyof typeof valid_link_options} LinkOptionName */
|
|
|
|
const valid_link_options = /** @type {const} */ ({
|
|
'preload-code': ['', 'off', 'false', 'tap', 'hover', 'viewport', 'eager'],
|
|
'preload-data': ['', 'off', 'false', 'tap', 'hover'],
|
|
keepfocus: ['', 'true', 'off', 'false'],
|
|
noscroll: ['', 'true', 'off', 'false'],
|
|
reload: ['', 'true', 'off', 'false'],
|
|
replacestate: ['', 'true', 'off', 'false']
|
|
});
|
|
|
|
/**
|
|
* @template {LinkOptionName} T
|
|
* @typedef {typeof valid_link_options[T][number]} ValidLinkOptions
|
|
*/
|
|
|
|
/**
|
|
* @template {LinkOptionName} T
|
|
* @param {Element} element
|
|
* @param {T} name
|
|
*/
|
|
function link_option(element, name) {
|
|
const value = /** @type {ValidLinkOptions<T> | null} */ (
|
|
element.getAttribute(`data-sveltekit-${name}`)
|
|
);
|
|
|
|
if (DEV) {
|
|
validate_link_option(element, name, value);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* @template {LinkOptionName} T
|
|
* @template {ValidLinkOptions<T> | null} U
|
|
* @param {Element} element
|
|
* @param {T} name
|
|
* @param {U} value
|
|
*/
|
|
function validate_link_option(element, name, value) {
|
|
if (value === null) return;
|
|
|
|
// @ts-expect-error - includes is dumb
|
|
if (!warned.has(element) && !valid_link_options[name].includes(value)) {
|
|
console.error(
|
|
`Unexpected value for ${name} — should be one of ${valid_link_options[name]
|
|
.map((option) => JSON.stringify(option))
|
|
.join(', ')}`,
|
|
element
|
|
);
|
|
|
|
warned.add(element);
|
|
}
|
|
}
|
|
|
|
const levels = {
|
|
...PRELOAD_PRIORITIES,
|
|
'': PRELOAD_PRIORITIES.hover
|
|
};
|
|
|
|
/**
|
|
* @param {Element} element
|
|
* @returns {Element | null}
|
|
*/
|
|
function parent_element(element) {
|
|
let parent = element.assignedSlot ?? element.parentNode;
|
|
|
|
// @ts-expect-error handle shadow roots
|
|
if (parent?.nodeType === 11) parent = parent.host;
|
|
|
|
return /** @type {Element} */ (parent);
|
|
}
|
|
|
|
/**
|
|
* @param {Element} element
|
|
* @param {Element} target
|
|
*/
|
|
export function find_anchor(element, target) {
|
|
while (element && element !== target) {
|
|
if (element.nodeName.toUpperCase() === 'A' && element.hasAttribute('href')) {
|
|
return /** @type {HTMLAnchorElement | SVGAElement} */ (element);
|
|
}
|
|
|
|
element = /** @type {Element} */ (parent_element(element));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLAnchorElement | SVGAElement} a
|
|
* @param {string} base
|
|
* @param {boolean} uses_hash_router
|
|
*/
|
|
export function get_link_info(a, base, uses_hash_router) {
|
|
/** @type {URL | undefined} */
|
|
let url;
|
|
|
|
try {
|
|
url = new URL(a instanceof SVGAElement ? a.href.baseVal : a.href, document.baseURI);
|
|
|
|
// if the hash doesn't start with `#/` then it's probably linking to an id on the current page
|
|
if (uses_hash_router && url.hash.match(/^#[^/]/)) {
|
|
const route = location.hash.split('#')[1] || '/';
|
|
url.hash = `#${route}${url.hash}`;
|
|
}
|
|
} catch {}
|
|
|
|
const target = a instanceof SVGAElement ? a.target.baseVal : a.target;
|
|
|
|
const external =
|
|
!url ||
|
|
!!target ||
|
|
is_external_url(url, base, uses_hash_router) ||
|
|
(a.getAttribute('rel') || '').split(/\s+/).includes('external');
|
|
|
|
const download = url?.origin === origin && a.hasAttribute('download');
|
|
|
|
return { url, external, target, download };
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLFormElement | HTMLAnchorElement | SVGAElement} element
|
|
*/
|
|
export function get_router_options(element) {
|
|
/** @type {ValidLinkOptions<'keepfocus'> | null} */
|
|
let keepfocus = null;
|
|
|
|
/** @type {ValidLinkOptions<'noscroll'> | null} */
|
|
let noscroll = null;
|
|
|
|
/** @type {ValidLinkOptions<'preload-code'> | null} */
|
|
let preload_code = null;
|
|
|
|
/** @type {ValidLinkOptions<'preload-data'> | null} */
|
|
let preload_data = null;
|
|
|
|
/** @type {ValidLinkOptions<'reload'> | null} */
|
|
let reload = null;
|
|
|
|
/** @type {ValidLinkOptions<'replacestate'> | null} */
|
|
let replace_state = null;
|
|
|
|
/** @type {Element} */
|
|
let el = element;
|
|
|
|
while (el && el !== document.documentElement) {
|
|
if (preload_code === null) preload_code = link_option(el, 'preload-code');
|
|
if (preload_data === null) preload_data = link_option(el, 'preload-data');
|
|
if (keepfocus === null) keepfocus = link_option(el, 'keepfocus');
|
|
if (noscroll === null) noscroll = link_option(el, 'noscroll');
|
|
if (reload === null) reload = link_option(el, 'reload');
|
|
if (replace_state === null) replace_state = link_option(el, 'replacestate');
|
|
|
|
el = /** @type {Element} */ (parent_element(el));
|
|
}
|
|
|
|
/** @param {string | null} value */
|
|
function get_option_state(value) {
|
|
switch (value) {
|
|
case '':
|
|
case 'true':
|
|
return true;
|
|
case 'off':
|
|
case 'false':
|
|
return false;
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return {
|
|
preload_code: levels[preload_code ?? 'off'],
|
|
preload_data: levels[preload_data ?? 'off'],
|
|
keepfocus: get_option_state(keepfocus),
|
|
noscroll: get_option_state(noscroll),
|
|
reload: get_option_state(reload),
|
|
replace_state: get_option_state(replace_state)
|
|
};
|
|
}
|
|
|
|
/** @param {any} value */
|
|
export 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 };
|
|
}
|
|
|
|
export const updated_listener = {
|
|
v: () => {}
|
|
};
|
|
|
|
export function create_updated_store() {
|
|
const { set, subscribe } = writable(false);
|
|
|
|
if (DEV || !BROWSER) {
|
|
return {
|
|
subscribe,
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
check: async () => false
|
|
};
|
|
}
|
|
|
|
const interval = __SVELTEKIT_APP_VERSION_POLL_INTERVAL__;
|
|
|
|
/** @type {NodeJS.Timeout} */
|
|
let timeout;
|
|
|
|
/** @type {() => Promise<boolean>} */
|
|
async function check() {
|
|
clearTimeout(timeout);
|
|
|
|
if (interval) timeout = setTimeout(check, interval);
|
|
|
|
try {
|
|
const res = await fetch(`${assets}/${__SVELTEKIT_APP_VERSION_FILE__}`, {
|
|
headers: {
|
|
pragma: 'no-cache',
|
|
'cache-control': 'no-cache'
|
|
}
|
|
});
|
|
|
|
if (!res.ok) {
|
|
return false;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const updated = data.version !== version;
|
|
|
|
if (updated) {
|
|
set(true);
|
|
updated_listener.v();
|
|
clearTimeout(timeout);
|
|
}
|
|
|
|
return updated;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (interval) timeout = setTimeout(check, interval);
|
|
|
|
return {
|
|
subscribe,
|
|
check
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Is external if
|
|
* - origin different
|
|
* - path doesn't start with base
|
|
* - uses hash router and pathname is more than base
|
|
* @param {URL} url
|
|
* @param {string} base
|
|
* @param {boolean} hash_routing
|
|
*/
|
|
export function is_external_url(url, base, hash_routing) {
|
|
if (url.origin !== origin || !url.pathname.startsWith(base)) {
|
|
return true;
|
|
}
|
|
|
|
if (hash_routing) {
|
|
if (url.pathname === base + '/' || url.pathname === base + '/index.html') {
|
|
return false;
|
|
}
|
|
|
|
// be lenient if serving from filesystem
|
|
if (url.protocol === 'file:' && url.pathname.replace(/\/[^/]+\.html?$/, '') === base) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** @type {Record<string, boolean>} */
|
|
const seen = {};
|
|
|
|
/**
|
|
* Used for server-side resolution, to replicate Vite's CSS loading behaviour in production.
|
|
*
|
|
* Closely modelled after https://github.com/vitejs/vite/blob/3dd12f4724130fdf8ba44c6d3252ebdff407fd47/packages/vite/src/node/plugins/importAnalysisBuild.ts#L214
|
|
* (which ideally we could just use directly, but it's not exported)
|
|
* @param {string[]} deps
|
|
*/
|
|
export function load_css(deps) {
|
|
if (__SVELTEKIT_CLIENT_ROUTING__) return;
|
|
|
|
const csp_nonce_meta = /** @type {HTMLMetaElement} */ (
|
|
document.querySelector('meta[property=csp-nonce]')
|
|
);
|
|
const csp_nonce = csp_nonce_meta?.nonce || csp_nonce_meta?.getAttribute('nonce');
|
|
|
|
for (const dep of deps) {
|
|
if (dep in seen) continue;
|
|
seen[dep] = true;
|
|
|
|
if (document.querySelector(`link[href="${dep}"][rel="stylesheet"]`)) {
|
|
continue;
|
|
}
|
|
|
|
const link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.crossOrigin = '';
|
|
link.href = dep;
|
|
if (csp_nonce) {
|
|
link.setAttribute('nonce', csp_nonce);
|
|
}
|
|
document.head.appendChild(link);
|
|
}
|
|
}
|