This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
/* if `bundleStrategy` is 'single' or 'inline', this file is used as the entry point */
import * as kit from './entry.js';
// @ts-expect-error
import * as app from '__sveltekit/manifest';
/**
*
* @param {HTMLElement} element
* @param {import('./types.js').HydrateOptions} options
*/
export function start(element, options) {
void kit.start(app, element, options);
}
export { app };
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
export const SNAPSHOT_KEY = 'sveltekit:snapshot';
export const SCROLL_KEY = 'sveltekit:scroll';
export const STATES_KEY = 'sveltekit:states';
export const PAGE_URL_KEY = 'sveltekit:pageurl';
export const HISTORY_INDEX = 'sveltekit:history';
export const NAVIGATION_INDEX = 'sveltekit:navigation';
export const PRELOAD_PRIORITIES = /** @type {const} */ ({
tap: 1,
hover: 2,
viewport: 3,
eager: 4,
off: -1,
false: -1
});
+3
View File
@@ -0,0 +1,3 @@
// we expose this as a separate entry point (rather than treating client.js as the entry point)
// so that everything other than `start`/`load_css` can be treeshaken
export { start, load_css } from './client.js';
+178
View File
@@ -0,0 +1,178 @@
import { BROWSER, DEV } from 'esm-env';
import { hash } from '../../utils/hash.js';
import { base64_decode } from '../utils.js';
let loading = 0;
/** @type {typeof fetch} */
const native_fetch = BROWSER ? window.fetch : /** @type {any} */ (() => {});
export function lock_fetch() {
loading += 1;
}
export function unlock_fetch() {
loading -= 1;
}
if (DEV && BROWSER) {
let can_inspect_stack_trace = false;
// detect whether async stack traces work
// eslint-disable-next-line @typescript-eslint/require-await
const check_stack_trace = async () => {
const stack = /** @type {string} */ (new Error().stack);
can_inspect_stack_trace = stack.includes('check_stack_trace');
};
void check_stack_trace();
/**
* @param {RequestInfo | URL} input
* @param {RequestInit & Record<string, any> | undefined} init
*/
window.fetch = (input, init) => {
// Check if fetch was called via load_node. the lock method only checks if it was called at the
// same time, but not necessarily if it was called from `load`.
// We use just the filename as the method name sometimes does not appear on the CI.
const url = input instanceof Request ? input.url : input.toString();
const stack_array = /** @type {string} */ (new Error().stack).split('\n');
// We need to do a cutoff because Safari and Firefox maintain the stack
// across events and for example traces a `fetch` call triggered from a button
// back to the creation of the event listener and the element creation itself,
// where at some point client.js will show up, leading to false positives.
const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load'));
const stack = stack_array.slice(0, cutoff + 2).join('\n');
const in_load_heuristic = can_inspect_stack_trace
? stack.includes('src/runtime/client/client.js')
: loading;
// This flag is set in initial_fetch and subsequent_fetch
const used_kit_fetch = init?.__sveltekit_fetch__;
if (in_load_heuristic && !used_kit_fetch) {
console.warn(
`Loading ${url} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://svelte.dev/docs/kit/load#making-fetch-requests`
);
}
const method = input instanceof Request ? input.method : init?.method || 'GET';
if (method !== 'GET') {
cache.delete(build_selector(input));
}
return native_fetch(input, init);
};
} else if (BROWSER) {
window.fetch = (input, init) => {
const method = input instanceof Request ? input.method : init?.method || 'GET';
if (method !== 'GET') {
cache.delete(build_selector(input));
}
return native_fetch(input, init);
};
}
const cache = new Map();
/**
* Should be called on the initial run of load functions that hydrate the page.
* Saves any requests with cache-control max-age to the cache.
* @param {URL | string} resource
* @param {RequestInit} [opts]
*/
export function initial_fetch(resource, opts) {
const selector = build_selector(resource, opts);
const script = document.querySelector(selector);
if (script?.textContent) {
script.remove(); // In case multiple script tags match the same selector
let { body, ...init } = JSON.parse(script.textContent);
const ttl = script.getAttribute('data-ttl');
if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) });
const b64 = script.getAttribute('data-b64');
if (b64 !== null) {
// Can't use native_fetch('data:...;base64,${body}')
// csp can block the request
body = base64_decode(body);
}
return Promise.resolve(new Response(body, init));
}
return DEV ? dev_fetch(resource, opts) : window.fetch(resource, opts);
}
/**
* Tries to get the response from the cache, if max-age allows it, else does a fetch.
* @param {URL | string} resource
* @param {string} resolved
* @param {RequestInit} [opts]
*/
export function subsequent_fetch(resource, resolved, opts) {
if (cache.size > 0) {
const selector = build_selector(resource, opts);
const cached = cache.get(selector);
if (cached) {
// https://developer.mozilla.org/en-US/docs/Web/API/Request/cache#value
if (
performance.now() < cached.ttl &&
['default', 'force-cache', 'only-if-cached', undefined].includes(opts?.cache)
) {
return new Response(cached.body, cached.init);
}
cache.delete(selector);
}
}
return DEV ? dev_fetch(resolved, opts) : window.fetch(resolved, opts);
}
/**
* @param {RequestInfo | URL} resource
* @param {RequestInit & Record<string, any> | undefined} opts
*/
export function dev_fetch(resource, opts) {
const patched_opts = { ...opts };
// This assigns the __sveltekit_fetch__ flag and makes it non-enumerable
Object.defineProperty(patched_opts, '__sveltekit_fetch__', {
value: true,
writable: true,
configurable: true
});
return window.fetch(resource, patched_opts);
}
/**
* Build the cache key for a given request
* @param {URL | RequestInfo} resource
* @param {RequestInit} [opts]
*/
function build_selector(resource, opts) {
const url = JSON.stringify(resource instanceof Request ? resource.url : resource);
let selector = `script[data-sveltekit-fetched][data-url=${url}]`;
if (opts?.headers || opts?.body) {
/** @type {import('types').StrictBody[]} */
const values = [];
if (opts.headers) {
values.push([...new Headers(opts.headers)].join(','));
}
if (opts.body && (typeof opts.body === 'string' || ArrayBuffer.isView(opts.body))) {
values.push(opts.body);
}
selector += `[data-hash="${hash(...values)}"]`;
}
return selector;
}
+77
View File
@@ -0,0 +1,77 @@
import { exec, parse_route_id } from '../../utils/routing.js';
/**
* @param {import('./types.js').SvelteKitApp} app
* @returns {import('types').CSRRoute[]}
*/
export function parse({ nodes, server_loads, dictionary, matchers }) {
const layouts_with_server_load = new Set(server_loads);
return Object.entries(dictionary).map(([id, [leaf, layouts, errors]]) => {
const { pattern, params } = parse_route_id(id);
/** @type {import('types').CSRRoute} */
const route = {
id,
/** @param {string} path */
exec: (path) => {
const match = pattern.exec(path);
if (match) return exec(match, params, matchers);
},
errors: [1, ...(errors || [])].map((n) => nodes[n]),
layouts: [0, ...(layouts || [])].map(create_layout_loader),
leaf: create_leaf_loader(leaf)
};
// bit of a hack, but ensures that layout/error node lists are the same
// length, without which the wrong data will be applied if the route
// manifest looks like `[[a, b], [c,], d]`
route.errors.length = route.layouts.length = Math.max(
route.errors.length,
route.layouts.length
);
return route;
});
/**
* @param {number} id
* @returns {[boolean, import('types').CSRPageNodeLoader]}
*/
function create_leaf_loader(id) {
// whether or not the route uses the server data is
// encoded using the ones' complement, to save space
const uses_server_data = id < 0;
if (uses_server_data) id = ~id;
return [uses_server_data, nodes[id]];
}
/**
* @param {number | undefined} id
* @returns {[boolean, import('types').CSRPageNodeLoader] | undefined}
*/
function create_layout_loader(id) {
// whether or not the layout uses the server data is
// encoded in the layouts array, to save space
return id === undefined ? id : [layouts_with_server_load.has(id), nodes[id]];
}
}
/**
* @param {import('types').CSRRouteServer} input
* @param {import('types').CSRPageNodeLoader[]} app_nodes Will be modified if a new node is loaded that's not already in the array
* @returns {import('types').CSRRoute}
*/
export function parse_server_route({ nodes, id, leaf, layouts, errors }, app_nodes) {
return {
id,
exec: () => ({}), // dummy function; exec already happened on the server
// By writing to app_nodes only when a loader at that index is not already defined,
// we ensure that loaders have referential equality when they load the same node.
// Code elsewhere in client.js relies on this referential equality to determine
// if a loader is different and should therefore (re-)run.
errors: errors.map((n) => (n ? (app_nodes[n] ||= nodes[n]) : undefined)),
layouts: layouts.map((n) => (n ? [n[0], (app_nodes[n[1]] ||= nodes[n[1]])] : undefined)),
leaf: [leaf[0], (app_nodes[leaf[1]] ||= nodes[leaf[1]])]
};
}
@@ -0,0 +1,93 @@
/** @import { RemoteCommand, RemoteQueryOverride } from '@sveltejs/kit' */
/** @import { RemoteFunctionResponse } from 'types' */
/** @import { Query } from './query.svelte.js' */
import { app_dir, base } from '$app/paths/internal/client';
import * as devalue from 'devalue';
import { HttpError } from '@sveltejs/kit/internal';
import { app } from '../client.js';
import { stringify_remote_arg } from '../../shared.js';
import { refresh_queries, release_overrides } from './shared.svelte.js';
/**
* Client-version of the `command` function from `$app/server`.
* @param {string} id
* @returns {RemoteCommand<any, any>}
*/
export function command(id) {
/** @type {number} */
let pending_count = $state(0);
// Careful: This function MUST be synchronous (can't use the async keyword) because the return type has to be a promise with an updates() method.
// If we make it async, the return type will be a promise that resolves to a promise with an updates() method, which is not what we want.
/** @type {RemoteCommand<any, any>} */
const command_function = (arg) => {
/** @type {Array<Query<any> | RemoteQueryOverride>} */
let updates = [];
// Increment pending count when command starts
pending_count++;
/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
const promise = (async () => {
try {
// Wait a tick to give room for the `updates` method to be called
await Promise.resolve();
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
method: 'POST',
body: JSON.stringify({
payload: stringify_remote_arg(arg, app.hooks.transport),
refreshes: updates.map((u) => u._key)
}),
headers: {
'Content-Type': 'application/json',
'x-sveltekit-pathname': location.pathname,
'x-sveltekit-search': location.search
}
});
if (!response.ok) {
release_overrides(updates);
// We only end up here in case of a network error or if the server has an internal error
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
throw new Error('Failed to execute remote function');
}
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
if (result.type === 'redirect') {
release_overrides(updates);
throw new Error(
'Redirects are not allowed in commands. Return a result instead and use goto on the client'
);
} else if (result.type === 'error') {
release_overrides(updates);
throw new HttpError(result.status ?? 500, result.error);
} else {
if (result.refreshes) {
refresh_queries(result.refreshes, updates);
}
return devalue.parse(result.result, app.decoders);
}
} finally {
// Decrement pending count when command completes
pending_count--;
}
})();
promise.updates = (/** @type {any} */ ...args) => {
updates = args;
// @ts-expect-error Don't allow updates to be called multiple times
delete promise.updates;
return promise;
};
return promise;
};
Object.defineProperty(command_function, 'pending', {
get: () => pending_count
});
return command_function;
}
@@ -0,0 +1,623 @@
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
/** @import { RemoteFormInput, RemoteForm, RemoteQueryOverride } from '@sveltejs/kit' */
/** @import { InternalRemoteFormIssue, RemoteFunctionResponse } from 'types' */
/** @import { Query } from './query.svelte.js' */
import { app_dir, base } from '$app/paths/internal/client';
import * as devalue from 'devalue';
import { DEV } from 'esm-env';
import { HttpError } from '@sveltejs/kit/internal';
import { app, remote_responses, _goto, set_nearest_error_page, invalidateAll } from '../client.js';
import { tick } from 'svelte';
import { refresh_queries, release_overrides } from './shared.svelte.js';
import { createAttachmentKey } from 'svelte/attachments';
import {
convert_formdata,
flatten_issues,
create_field_proxy,
deep_set,
set_nested_value,
throw_on_old_property_access,
build_path_string,
normalize_issue,
serialize_binary_form,
BINARY_FORM_CONTENT_TYPE
} from '../../form-utils.js';
/**
* Merge client issues into server issues. Server issues are persisted unless
* a client-issue exists for the same path, in which case the client-issue overrides it.
* @param {FormData} form_data
* @param {InternalRemoteFormIssue[]} current_issues
* @param {InternalRemoteFormIssue[]} client_issues
* @returns {InternalRemoteFormIssue[]}
*/
function merge_with_server_issues(form_data, current_issues, client_issues) {
const merged = [
...current_issues.filter(
(issue) => issue.server && !client_issues.some((i) => i.name === issue.name)
),
...client_issues
];
const keys = Array.from(form_data.keys());
return merged.sort((a, b) => keys.indexOf(a.name) - keys.indexOf(b.name));
}
/**
* Client-version of the `form` function from `$app/server`.
* @template {RemoteFormInput} T
* @template U
* @param {string} id
* @returns {RemoteForm<T, U>}
*/
export function form(id) {
/** @type {Map<any, { count: number, instance: RemoteForm<T, U> }>} */
const instances = new Map();
/** @param {string | number | boolean} [key] */
function create_instance(key) {
const action_id_without_key = id;
const action_id = id + (key != undefined ? `/${JSON.stringify(key)}` : '');
const action = '?/remote=' + encodeURIComponent(action_id);
/**
* @type {Record<string, string | string[] | File | File[]>}
*/
let input = $state({});
/** @type {InternalRemoteFormIssue[]} */
let raw_issues = $state.raw([]);
const issues = $derived(flatten_issues(raw_issues));
/** @type {any} */
let result = $state.raw(remote_responses[action_id]);
/** @type {number} */
let pending_count = $state(0);
/** @type {StandardSchemaV1 | undefined} */
let preflight_schema = undefined;
/** @type {HTMLFormElement | null} */
let element = null;
/** @type {Record<string, boolean>} */
let touched = {};
let submitted = false;
/**
* @param {FormData} form_data
* @returns {Record<string, any>}
*/
function convert(form_data) {
const data = convert_formdata(form_data);
if (key !== undefined && !form_data.has('id')) {
data.id = key;
}
return data;
}
/**
* @param {HTMLFormElement} form
* @param {FormData} form_data
* @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback
*/
async function handle_submit(form, form_data, callback) {
const data = convert(form_data);
submitted = true;
const validated = await preflight_schema?.['~standard'].validate(data);
if (validated?.issues) {
raw_issues = merge_with_server_issues(
form_data,
raw_issues,
validated.issues.map((issue) => normalize_issue(issue, false))
);
return;
}
// TODO 3.0 remove this warning
if (DEV) {
const error = () => {
throw new Error(
'Remote form functions no longer get passed a FormData object. The payload is now a POJO. See https://kit.svelte.dev/docs/remote-functions#form for details.'
);
};
for (const key of [
'append',
'delete',
'entries',
'forEach',
'get',
'getAll',
'has',
'keys',
'set',
'values'
]) {
if (!(key in data)) {
Object.defineProperty(data, key, { get: error });
}
}
}
try {
await callback({
form,
data,
submit: () => submit(form_data)
});
} catch (e) {
const error = e instanceof HttpError ? e.body : { message: /** @type {any} */ (e).message };
const status = e instanceof HttpError ? e.status : 500;
void set_nearest_error_page(error, status);
}
}
/**
* @param {FormData} data
* @returns {Promise<any> & { updates: (...args: any[]) => any }}
*/
function submit(data) {
// Store a reference to the current instance and increment the usage count for the duration
// of the request. This ensures that the instance is not deleted in case of an optimistic update
// (e.g. when deleting an item in a list) that fails and wants to surface an error to the user afterwards.
// If the instance would be deleted in the meantime, the error property would be assigned to the old,
// no-longer-visible instance, so it would never be shown to the user.
const entry = instances.get(key);
if (entry) {
entry.count++;
}
// Increment pending count when submission starts
pending_count++;
/** @type {Array<Query<any> | RemoteQueryOverride>} */
let updates = [];
/** @type {Promise<any> & { updates: (...args: any[]) => any }} */
const promise = (async () => {
try {
await Promise.resolve();
const { blob } = serialize_binary_form(convert(data), {
remote_refreshes: updates.map((u) => u._key)
});
const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
method: 'POST',
headers: {
'Content-Type': BINARY_FORM_CONTENT_TYPE,
'x-sveltekit-pathname': location.pathname,
'x-sveltekit-search': location.search
},
body: blob
});
if (!response.ok) {
// We only end up here in case of a network error or if the server has an internal error
// (which shouldn't happen because we handle errors on the server and always send a 200 response)
throw new Error('Failed to execute remote function');
}
const form_result = /** @type { RemoteFunctionResponse} */ (await response.json());
// reset issues in case it's a redirect or error (but issues passed in that case)
raw_issues = [];
if (form_result.type === 'result') {
({ issues: raw_issues = [], result } = devalue.parse(form_result.result, app.decoders));
if (issues.$) {
release_overrides(updates);
} else {
if (form_result.refreshes) {
refresh_queries(form_result.refreshes, updates);
} else {
void invalidateAll();
}
}
} else if (form_result.type === 'redirect') {
const refreshes = form_result.refreshes ?? '';
const invalidateAll = !refreshes && updates.length === 0;
if (!invalidateAll) {
refresh_queries(refreshes, updates);
}
// Use internal version to allow redirects to external URLs
void _goto(form_result.location, { invalidateAll }, 0);
} else {
throw new HttpError(form_result.status ?? 500, form_result.error);
}
} catch (e) {
result = undefined;
release_overrides(updates);
throw e;
} finally {
// Decrement pending count when submission completes
pending_count--;
void tick().then(() => {
if (entry) {
entry.count--;
if (entry.count === 0) {
instances.delete(key);
}
}
});
}
})();
promise.updates = (...args) => {
updates = args;
return promise;
};
return promise;
}
/** @type {RemoteForm<T, U>} */
const instance = {};
instance.method = 'POST';
instance.action = action;
/** @param {Parameters<RemoteForm<any, any>['enhance']>[0]} callback */
const form_onsubmit = (callback) => {
/** @param {SubmitEvent} event */
return async (event) => {
const form = /** @type {HTMLFormElement} */ (event.target);
const method = event.submitter?.hasAttribute('formmethod')
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formMethod
: clone(form).method;
if (method !== 'post') return;
const action = new URL(
// We can't do submitter.formAction directly because that property is always set
event.submitter?.hasAttribute('formaction')
? /** @type {HTMLButtonElement | HTMLInputElement} */ (event.submitter).formAction
: clone(form).action
);
if (action.searchParams.get('/remote') !== action_id) {
return;
}
event.preventDefault();
const form_data = new FormData(form, event.submitter);
if (DEV) {
validate_form_data(form_data, clone(form).enctype);
}
await handle_submit(form, form_data, callback);
};
};
/** @param {(event: SubmitEvent) => void} onsubmit */
function create_attachment(onsubmit) {
return (/** @type {HTMLFormElement} */ form) => {
if (element) {
let message = `A form object can only be attached to a single \`<form>\` element`;
if (DEV && !key) {
const name = id.split('/').pop();
message += `. To create multiple instances, use \`${name}.for(key)\``;
}
throw new Error(message);
}
element = form;
touched = {};
form.addEventListener('submit', onsubmit);
form.addEventListener('input', (e) => {
// strictly speaking it can be an HTMLTextAreaElement or HTMLSelectElement
// but that makes the types unnecessarily awkward
const element = /** @type {HTMLInputElement} */ (e.target);
let name = element.name;
if (!name) return;
const is_array = name.endsWith('[]');
if (is_array) name = name.slice(0, -2);
const is_file = element.type === 'file';
touched[name] = true;
if (is_array) {
let value;
if (element.tagName === 'SELECT') {
value = Array.from(
element.querySelectorAll('option:checked'),
(e) => /** @type {HTMLOptionElement} */ (e).value
);
} else {
const elements = /** @type {HTMLInputElement[]} */ (
Array.from(form.querySelectorAll(`[name="${name}[]"]`))
);
if (DEV) {
for (const e of elements) {
if ((e.type === 'file') !== is_file) {
throw new Error(
`Cannot mix and match file and non-file inputs under the same name ("${element.name}")`
);
}
}
}
value = is_file
? elements.map((input) => Array.from(input.files ?? [])).flat()
: elements.map((element) => element.value);
if (element.type === 'checkbox') {
value = /** @type {string[]} */ (value.filter((_, i) => elements[i].checked));
}
}
set_nested_value(input, name, value);
} else if (is_file) {
if (DEV && element.multiple) {
throw new Error(
`Can only use the \`multiple\` attribute when \`name\` includes a \`[]\` suffix — consider changing "${name}" to "${name}[]"`
);
}
const file = /** @type {HTMLInputElement & { files: FileList }} */ (element).files[0];
if (file) {
set_nested_value(input, name, file);
} else {
// Remove the property by setting to undefined and clean up
const path_parts = name.split(/\.|\[|\]/).filter(Boolean);
let current = /** @type {any} */ (input);
for (let i = 0; i < path_parts.length - 1; i++) {
if (current[path_parts[i]] == null) return;
current = current[path_parts[i]];
}
delete current[path_parts[path_parts.length - 1]];
}
} else {
set_nested_value(
input,
name,
element.type === 'checkbox' && !element.checked ? null : element.value
);
}
name = name.replace(/^[nb]:/, '');
touched[name] = true;
});
form.addEventListener('reset', async () => {
// need to wait a moment, because the `reset` event occurs before
// the inputs are actually updated (so that it can be cancelled)
await tick();
input = convert_formdata(new FormData(form));
});
return () => {
element = null;
preflight_schema = undefined;
};
};
}
instance[createAttachmentKey()] = create_attachment(
form_onsubmit(({ submit, form }) =>
submit().then(() => {
if (!issues.$) {
form.reset();
}
})
)
);
let validate_id = 0;
// TODO 3.0 remove
if (DEV) {
throw_on_old_property_access(instance);
Object.defineProperty(instance, 'buttonProps', {
get() {
throw new Error(
'`form.buttonProps` has been removed: Instead of `<button {...form.buttonProps}>, use `<button {...form.fields.action.as("submit", "value")}>`.' +
' See the PR for more info: https://github.com/sveltejs/kit/pull/14622'
);
}
});
}
Object.defineProperties(instance, {
fields: {
get: () =>
create_field_proxy(
{},
() => input,
(path, value) => {
if (path.length === 0) {
input = value;
} else {
deep_set(input, path.map(String), value);
const key = build_path_string(path);
touched[key] = true;
}
},
() => issues
)
},
result: {
get: () => result
},
pending: {
get: () => pending_count
},
preflight: {
/** @type {RemoteForm<T, U>['preflight']} */
value: (schema) => {
preflight_schema = schema;
return instance;
}
},
validate: {
/** @type {RemoteForm<any, any>['validate']} */
value: async ({ includeUntouched = false, preflightOnly = false } = {}) => {
if (!element) return;
const id = ++validate_id;
// wait a tick in case the user is calling validate() right after set() which takes time to propagate
await tick();
const default_submitter = /** @type {HTMLElement | undefined} */ (
element.querySelector('button:not([type]), [type="submit"]')
);
const form_data = new FormData(element, default_submitter);
/** @type {InternalRemoteFormIssue[]} */
let array = [];
const data = convert(form_data);
const validated = await preflight_schema?.['~standard'].validate(data);
if (validate_id !== id) {
return;
}
if (validated?.issues) {
array = validated.issues.map((issue) => normalize_issue(issue, false));
} else if (!preflightOnly) {
const response = await fetch(`${base}/${app_dir}/remote/${action_id_without_key}`, {
method: 'POST',
headers: {
'Content-Type': BINARY_FORM_CONTENT_TYPE,
'x-sveltekit-pathname': location.pathname,
'x-sveltekit-search': location.search
},
body: serialize_binary_form(data, {
validate_only: true
}).blob
});
const result = await response.json();
if (validate_id !== id) {
return;
}
if (result.type === 'result') {
array = /** @type {InternalRemoteFormIssue[]} */ (
devalue.parse(result.result, app.decoders)
);
}
}
if (!includeUntouched && !submitted) {
array = array.filter((issue) => touched[issue.name]);
}
const is_server_validation = !validated?.issues && !preflightOnly;
raw_issues = is_server_validation
? array
: merge_with_server_issues(form_data, raw_issues, array);
}
},
enhance: {
/** @type {RemoteForm<any, any>['enhance']} */
value: (callback) => {
return {
method: 'POST',
action,
[createAttachmentKey()]: create_attachment(form_onsubmit(callback))
};
}
}
});
return instance;
}
const instance = create_instance();
Object.defineProperty(instance, 'for', {
/** @type {RemoteForm<T, U>['for']} */
value: (key) => {
const entry = instances.get(key) ?? { count: 0, instance: create_instance(key) };
try {
$effect.pre(() => {
return () => {
entry.count--;
void tick().then(() => {
if (entry.count === 0) {
instances.delete(key);
}
});
};
});
entry.count += 1;
instances.set(key, entry);
} catch {
// not in an effect context
}
return entry.instance;
}
});
return instance;
}
/**
* Shallow clone an element, so that we can access e.g. `form.action` without worrying
* that someone has added an `<input name="action">` (https://github.com/sveltejs/kit/issues/7593)
* @template {HTMLElement} T
* @param {T} element
* @returns {T}
*/
function clone(element) {
return /** @type {T} */ (HTMLElement.prototype.cloneNode.call(element));
}
/**
* @param {FormData} form_data
* @param {string} enctype
*/
function validate_form_data(form_data, enctype) {
for (const key of form_data.keys()) {
if (/^\$[.[]?/.test(key)) {
throw new Error(
'`$` is used to collect all FormData validation issues and cannot be used as the `name` of a form control'
);
}
}
if (enctype !== 'multipart/form-data') {
for (const value of form_data.values()) {
if (value instanceof File) {
throw new Error(
'Your form contains <input type="file"> fields, but is missing the necessary `enctype="multipart/form-data"` attribute. This will lead to inconsistent behavior between enhanced and native forms. For more details, see https://github.com/sveltejs/kit/issues/9819.'
);
}
}
}
}
@@ -0,0 +1,4 @@
export { command } from './command.svelte.js';
export { form } from './form.svelte.js';
export { prerender } from './prerender.svelte.js';
export { query, query_batch } from './query.svelte.js';
@@ -0,0 +1,177 @@
import { app_dir, base } from '$app/paths/internal/client';
import { version } from '__sveltekit/environment';
import * as devalue from 'devalue';
import { DEV } from 'esm-env';
import { app, remote_responses } from '../client.js';
import { create_remote_function, remote_request } from './shared.svelte.js';
// Initialize Cache API for prerender functions
const CACHE_NAME = DEV ? `sveltekit:${Date.now()}` : `sveltekit:${version}`;
/** @type {Cache | undefined} */
let prerender_cache;
const prerender_cache_ready = (async () => {
if (typeof caches !== 'undefined') {
try {
prerender_cache = await caches.open(CACHE_NAME);
// Clean up old cache versions
const cache_names = await caches.keys();
for (const cache_name of cache_names) {
if (cache_name.startsWith('sveltekit:') && cache_name !== CACHE_NAME) {
await caches.delete(cache_name);
}
}
} catch (error) {
console.warn('Failed to initialize SvelteKit cache:', error);
}
}
})();
/**
* @template T
* @implements {Partial<Promise<T>>}
*/
class Prerender {
/** @type {Promise<T>} */
#promise;
#loading = $state(true);
#ready = $state(false);
/** @type {T | undefined} */
#current = $state.raw();
#error = $state.raw(undefined);
/**
* @param {() => Promise<T>} fn
*/
constructor(fn) {
this.#promise = fn().then(
(value) => {
this.#loading = false;
this.#ready = true;
this.#current = value;
return value;
},
(error) => {
this.#loading = false;
this.#error = error;
throw error;
}
);
}
/**
*
* @param {((value: any) => any) | null | undefined} onfulfilled
* @param {((reason: any) => any) | null | undefined} [onrejected]
* @returns
*/
then(onfulfilled, onrejected) {
return this.#promise.then(onfulfilled, onrejected);
}
/**
* @param {((reason: any) => any) | null | undefined} onrejected
*/
catch(onrejected) {
return this.#promise.catch(onrejected);
}
/**
* @param {(() => any) | null | undefined} onfinally
*/
finally(onfinally) {
return this.#promise.finally(onfinally);
}
get current() {
return this.#current;
}
get error() {
return this.#error;
}
/**
* Returns true if the resource is loading.
*/
get loading() {
return this.#loading;
}
/**
* Returns true once the resource has been loaded.
*/
get ready() {
return this.#ready;
}
}
/**
* @param {string} url
* @param {string} encoded
*/
function put(url, encoded) {
return /** @type {Cache} */ (prerender_cache)
.put(
url,
// We need to create a new response because the original response is already consumed
new Response(encoded, {
headers: {
'Content-Type': 'application/json'
}
})
)
.catch(() => {
// Nothing we can do here
});
}
/**
* @param {string} id
*/
export function prerender(id) {
return create_remote_function(id, (cache_key, payload) => {
return new Prerender(async () => {
await prerender_cache_ready;
const url = `${base}/${app_dir}/remote/${id}${payload ? `/${payload}` : ''}`;
if (Object.hasOwn(remote_responses, cache_key)) {
const data = remote_responses[cache_key];
if (prerender_cache) {
void put(url, devalue.stringify(data, app.encoders));
}
return data;
}
// Check the Cache API first
if (prerender_cache) {
try {
const cached_response = await prerender_cache.match(url);
if (cached_response) {
const cached_result = await cached_response.text();
return devalue.parse(cached_result, app.decoders);
}
} catch {
// Nothing we can do here
}
}
const encoded = await remote_request(url);
// For successful prerender requests, save to cache
if (prerender_cache) {
void put(url, encoded);
}
return devalue.parse(encoded, app.decoders);
});
});
}
@@ -0,0 +1,328 @@
/** @import { RemoteQueryFunction } from '@sveltejs/kit' */
/** @import { RemoteFunctionResponse } from 'types' */
import { app_dir, base } from '$app/paths/internal/client';
import { app, goto, query_map, remote_responses } from '../client.js';
import { tick } from 'svelte';
import { create_remote_function, remote_request } from './shared.svelte.js';
import * as devalue from 'devalue';
import { HttpError, Redirect } from '@sveltejs/kit/internal';
import { DEV } from 'esm-env';
/**
* @param {string} id
* @returns {RemoteQueryFunction<any, any>}
*/
export function query(id) {
if (DEV) {
// If this reruns as part of HMR, refresh the query
for (const [key, entry] of query_map) {
if (key === id || key.startsWith(id + '/')) {
// use optional chaining in case a prerender function was turned into a query
entry.resource.refresh?.();
}
}
}
return create_remote_function(id, (cache_key, payload) => {
return new Query(cache_key, async () => {
if (Object.hasOwn(remote_responses, cache_key)) {
return remote_responses[cache_key];
}
const url = `${base}/${app_dir}/remote/${id}${payload ? `?payload=${payload}` : ''}`;
const result = await remote_request(url);
return devalue.parse(result, app.decoders);
});
});
}
/**
* @param {string} id
* @returns {(arg: any) => Query<any>}
*/
export function query_batch(id) {
/** @type {Map<string, Array<{resolve: (value: any) => void, reject: (error: any) => void}>>} */
let batching = new Map();
return create_remote_function(id, (cache_key, payload) => {
return new Query(cache_key, () => {
if (Object.hasOwn(remote_responses, cache_key)) {
return remote_responses[cache_key];
}
// Collect all the calls to the same query in the same macrotask,
// then execute them as one backend request.
return new Promise((resolve, reject) => {
// create_remote_function caches identical calls, but in case a refresh to the same query is called multiple times this function
// is invoked multiple times with the same payload, so we need to deduplicate here
const entry = batching.get(payload) ?? [];
entry.push({ resolve, reject });
batching.set(payload, entry);
if (batching.size > 1) return;
// Wait for the next macrotask - don't use microtask as Svelte runtime uses these to collect changes and flush them,
// and flushes could reveal more queries that should be batched.
setTimeout(async () => {
const batched = batching;
batching = new Map();
try {
const response = await fetch(`${base}/${app_dir}/remote/${id}`, {
method: 'POST',
body: JSON.stringify({
payloads: Array.from(batched.keys())
}),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to execute batch query');
}
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
if (result.type === 'error') {
throw new HttpError(result.status ?? 500, result.error);
}
if (result.type === 'redirect') {
await goto(result.location);
throw new Redirect(307, result.location);
}
const results = devalue.parse(result.result, app.decoders);
// Resolve individual queries
// Maps guarantee insertion order so we can do it like this
let i = 0;
for (const resolvers of batched.values()) {
for (const { resolve, reject } of resolvers) {
if (results[i].type === 'error') {
reject(new HttpError(results[i].status, results[i].error));
} else {
resolve(results[i].data);
}
}
i++;
}
} catch (error) {
// Reject all queries in the batch
for (const resolver of batched.values()) {
for (const { reject } of resolver) {
reject(error);
}
}
}
}, 0);
});
});
});
}
/**
* @template T
* @implements {Partial<Promise<T>>}
*/
export class Query {
/** @type {string} */
_key;
#init = false;
/** @type {() => Promise<T>} */
#fn;
#loading = $state(true);
/** @type {Array<() => void>} */
#latest = [];
/** @type {boolean} */
#ready = $state(false);
/** @type {T | undefined} */
#raw = $state.raw();
/** @type {Promise<void>} */
#promise;
/** @type {Array<(old: T) => T>} */
#overrides = $state([]);
/** @type {T | undefined} */
#current = $derived.by(() => {
// don't reduce undefined value
if (!this.#ready) return undefined;
return this.#overrides.reduce((v, r) => r(v), /** @type {T} */ (this.#raw));
});
#error = $state.raw(undefined);
/** @type {Promise<T>['then']} */
// @ts-expect-error TS doesn't understand that the promise returns something
#then = $derived.by(() => {
const p = this.#promise;
this.#overrides.length;
return (resolve, reject) => {
const result = (async () => {
await p;
// svelte-ignore await_reactivity_loss
await tick();
return /** @type {T} */ (this.#current);
})();
if (resolve || reject) {
return result.then(resolve, reject);
}
return result;
};
});
/**
* @param {string} key
* @param {() => Promise<T>} fn
*/
constructor(key, fn) {
this._key = key;
this.#fn = fn;
this.#promise = $state.raw(this.#run());
}
#run() {
// Prevent state_unsafe_mutation error on first run when the resource is created within the template
if (this.#init) {
this.#loading = true;
} else {
this.#init = true;
}
// Don't use Promise.withResolvers, it's too new still
/** @type {() => void} */
let resolve;
/** @type {(e?: any) => void} */
let reject;
/** @type {Promise<void>} */
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.#latest.push(
// @ts-expect-error it's defined at this point
resolve
);
Promise.resolve(this.#fn())
.then((value) => {
// Skip the response if resource was refreshed with a later promise while we were waiting for this one to resolve
const idx = this.#latest.indexOf(resolve);
if (idx === -1) return;
this.#latest.splice(0, idx).forEach((r) => r());
this.#ready = true;
this.#loading = false;
this.#raw = value;
this.#error = undefined;
resolve();
})
.catch((e) => {
const idx = this.#latest.indexOf(resolve);
if (idx === -1) return;
this.#latest.splice(0, idx).forEach((r) => r());
this.#error = e;
this.#loading = false;
reject(e);
});
return promise;
}
get then() {
return this.#then;
}
get catch() {
this.#then;
return (/** @type {any} */ reject) => {
return this.#then(undefined, reject);
};
}
get finally() {
this.#then;
return (/** @type {any} */ fn) => {
return this.#then(
(value) => {
fn();
return value;
},
(error) => {
fn();
throw error;
}
);
};
}
get current() {
return this.#current;
}
get error() {
return this.#error;
}
/**
* Returns true if the resource is loading or reloading.
*/
get loading() {
return this.#loading;
}
/**
* Returns true once the resource has been loaded for the first time.
*/
get ready() {
return this.#ready;
}
/**
* @returns {Promise<void>}
*/
refresh() {
delete remote_responses[this._key];
return (this.#promise = this.#run());
}
/**
* @param {T} value
*/
set(value) {
this.#ready = true;
this.#loading = false;
this.#error = undefined;
this.#raw = value;
this.#promise = Promise.resolve();
}
/**
* @param {(old: T) => T} fn
*/
withOverride(fn) {
this.#overrides.push(fn);
return {
_key: this._key,
release: () => {
const i = this.#overrides.indexOf(fn);
if (i !== -1) {
this.#overrides.splice(i, 1);
}
}
};
}
}
@@ -0,0 +1,143 @@
/** @import { RemoteQueryOverride } from '@sveltejs/kit' */
/** @import { RemoteFunctionResponse } from 'types' */
/** @import { Query } from './query.svelte.js' */
import * as devalue from 'devalue';
import { app, goto, query_map, remote_responses } from '../client.js';
import { HttpError, Redirect } from '@sveltejs/kit/internal';
import { tick } from 'svelte';
import { create_remote_key, stringify_remote_arg } from '../../shared.js';
/**
*
* @param {string} url
*/
export async function remote_request(url) {
const response = await fetch(url, {
headers: {
// TODO in future, when we support forking, we will likely need
// to grab this from context as queries will run before
// `location.pathname` is updated
'x-sveltekit-pathname': location.pathname,
'x-sveltekit-search': location.search
}
});
if (!response.ok) {
throw new HttpError(500, 'Failed to execute remote function');
}
const result = /** @type {RemoteFunctionResponse} */ (await response.json());
if (result.type === 'redirect') {
await goto(result.location);
throw new Redirect(307, result.location);
}
if (result.type === 'error') {
throw new HttpError(result.status ?? 500, result.error);
}
return result.result;
}
/**
* Client-version of the `query`/`prerender`/`cache` function from `$app/server`.
* @param {string} id
* @param {(key: string, args: string) => any} create
*/
export function create_remote_function(id, create) {
return (/** @type {any} */ arg) => {
const payload = stringify_remote_arg(arg, app.hooks.transport);
const cache_key = create_remote_key(id, payload);
let entry = query_map.get(cache_key);
let tracking = true;
try {
$effect.pre(() => {
if (entry) entry.count++;
return () => {
const entry = query_map.get(cache_key);
if (entry) {
entry.count--;
void tick().then(() => {
if (!entry.count && entry === query_map.get(cache_key)) {
query_map.delete(cache_key);
delete remote_responses[cache_key];
}
});
}
};
});
} catch {
tracking = false;
}
let resource = entry?.resource;
if (!resource) {
resource = create(cache_key, payload);
Object.defineProperty(resource, '_key', {
value: cache_key
});
query_map.set(
cache_key,
(entry = {
count: tracking ? 1 : 0,
resource
})
);
resource
.then(() => {
void tick().then(() => {
if (
!(/** @type {NonNullable<typeof entry>} */ (entry).count) &&
entry === query_map.get(cache_key)
) {
// If no one is tracking this resource anymore, we can delete it from the cache
query_map.delete(cache_key);
}
});
})
.catch(() => {
// error delete the resource from the cache
// TODO is that correct?
query_map.delete(cache_key);
});
}
return resource;
};
}
/**
* @param {Array<Query<any> | RemoteQueryOverride>} updates
*/
export function release_overrides(updates) {
for (const update of updates) {
if ('release' in update) {
update.release();
}
}
}
/**
* @param {string} stringified_refreshes
* @param {Array<Query<any> | RemoteQueryOverride>} updates
*/
export function refresh_queries(stringified_refreshes, updates = []) {
const refreshes = Object.entries(devalue.parse(stringified_refreshes, app.decoders));
// `refreshes` is a superset of `updates`
for (const [key, value] of refreshes) {
// If there was an optimistic update, release it right before we update the query
const update = updates.find((u) => u._key === key);
if (update && 'release' in update) {
update.release();
}
// Update the query with the new value
const entry = query_map.get(key);
entry?.resource.set(value);
}
}
+28
View File
@@ -0,0 +1,28 @@
/**
* Read a value from `sessionStorage`
* @param {string} key
* @param {(value: string) => any} parse
*/
/*@__NO_SIDE_EFFECTS__*/
export function get(key, parse = JSON.parse) {
try {
return parse(sessionStorage[key]);
} catch {
// do nothing
}
}
/**
* Write a value to `sessionStorage`
* @param {string} key
* @param {any} value
* @param {(value: any) => string} stringify
*/
export function set(key, value, stringify = JSON.stringify) {
const data = stringify(value);
try {
sessionStorage[key] = data;
} catch {
// do nothing
}
}
+57
View File
@@ -0,0 +1,57 @@
import { onMount } from 'svelte';
import { updated_listener } from './utils.js';
/** @type {import('@sveltejs/kit').Page} */
export let page;
/** @type {{ current: import('@sveltejs/kit').Navigation | null }} */
export let navigating;
/** @type {{ current: boolean }} */
export let updated;
// this is a bootleg way to tell if we're in old svelte or new svelte
const is_legacy =
onMount.toString().includes('$$') || /function \w+\(\) \{\}/.test(onMount.toString());
if (is_legacy) {
page = {
data: {},
form: null,
error: null,
params: {},
route: { id: null },
state: {},
status: -1,
url: new URL('https://example.com')
};
navigating = { current: null };
updated = { current: false };
} else {
page = new (class Page {
data = $state.raw({});
form = $state.raw(null);
error = $state.raw(null);
params = $state.raw({});
route = $state.raw({ id: null });
state = $state.raw({});
status = $state.raw(-1);
url = $state.raw(new URL('https://example.com'));
})();
navigating = new (class Navigating {
current = $state.raw(null);
})();
updated = new (class Updated {
current = $state.raw(false);
})();
updated_listener.v = () => (updated.current = true);
}
/**
* @param {import('@sveltejs/kit').Page} new_page
*/
export function update(new_page) {
Object.assign(page, new_page);
}
+131
View File
@@ -0,0 +1,131 @@
import { SvelteComponent } from 'svelte';
import {
ClientHooks,
CSRPageNode,
CSRPageNodeLoader,
CSRRoute,
CSRRouteServer,
ServerDataNode,
TrailingSlash,
Uses
} from 'types';
import { Page, ParamMatcher } from '@sveltejs/kit';
export interface SvelteKitApp {
/**
* A list of all the error/layout/page nodes used in the app.
* - In case of router.resolution=client, this is filled completely upfront.
* - In case of router.resolution=server, this is filled with the root layout and root error page
* at the beginning and then filled up as the user navigates around the app, loading new nodes
*/
nodes: CSRPageNodeLoader[];
/**
* A list of all layout node ids that have a server load function.
* Pages are not present because it's shorter to encode it on the leaf itself.
*
* In case of router.resolution=server, this only contains one entry for the root layout.
*/
server_loads: number[];
/**
* A map of `[routeId: string]: [leaf, layouts, errors]` tuples, which
* is parsed into an array of routes on startup. The numbers refer to the indices in `nodes`.
* If the leaf number is negative, it means it does use a server load function and the complement is the node index.
* The route layout and error nodes are not referenced, they are always number 0 and 1 and always apply.
*
* In case of router.resolution=server, this object is empty, as resolution happens on the server.
*/
dictionary: Record<string, [leaf: number, layouts: number[], errors?: number[]]>;
/**
* A map of `[matcherName: string]: (..) => boolean`, which is used to match route parameters.
*
* In case of router.resolution=server, this object is empty, as resolution happens on the server.
*/
matchers: Record<string, ParamMatcher>;
hooks: ClientHooks;
decode: (type: string, value: any) => any;
decoders: Record<string, (data: any) => any>;
encoders: Record<string, (data: any) => any>;
/**
* Whether or not we're using hash-based routing
*/
hash: boolean;
root: typeof SvelteComponent;
}
export type NavigationIntent = {
/** `url.pathname + url.search` */
id: string;
/** Whether we are invalidating or navigating */
invalidating: boolean;
/** The route parameters */
params: Record<string, string>;
/** The route that matches `path` */
route: CSRRoute;
/** The destination URL */
url: URL;
};
export type NavigationResult = NavigationRedirect | NavigationFinished;
export type NavigationRedirect = {
type: 'redirect';
location: string;
};
export type NavigationFinished = {
type: 'loaded';
state: NavigationState;
props: {
constructors: Array<typeof SvelteComponent>;
components?: SvelteComponent[];
page: Page;
form?: Record<string, any> | null;
[key: `data_${number}`]: Record<string, any>;
};
};
export type BranchNode = {
node: CSRPageNode;
loader: CSRPageNodeLoader;
server: DataNode | null;
universal: DataNode | null;
data: Record<string, any> | null;
slash?: TrailingSlash;
};
export interface DataNode {
type: 'data';
data: Record<string, any> | null;
uses: Uses;
slash?: TrailingSlash;
}
export interface NavigationState {
branch: Array<BranchNode | undefined>;
error: App.Error | null;
params: Record<string, string>;
route: CSRRoute | null;
url: URL;
}
export interface HydrateOptions {
status: number;
error: App.Error | null;
node_ids: number[];
params: Record<string, string>;
route: { id: string | null };
/** Only used when `router.resolution=server`; can then still be undefined in case of 404 */
server_route?: CSRRouteServer;
data: Array<ServerDataNode | null>;
form: Record<string, any> | null;
/** The results of all remote functions executed during SSR so that they can be reused during hydration */
remote: Record<string, any>;
}
+365
View File
@@ -0,0 +1,365 @@
import { BROWSER, DEV } from 'esm-env';
import { writable } from 'svelte/store';
import { assets } from '$app/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) {
return url.pathname !== location.pathname;
}
return false;
}
/** @type {Set<string> | null} */
let seen = null;
/**
* 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');
seen ??= new Set(
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map((link) => {
return /** @type {HTMLLinkElement} */ (link).href;
})
);
for (const dep of deps) {
const href = new URL(dep, document.baseURI).href;
if (seen.has(href)) continue;
seen.add(href);
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);
}
}