@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
366 lines (304 loc) • 9.48 kB
JavaScript
import * as devalue from 'devalue';
import { base64_decode, base64_encode, text_decoder, text_encoder } from './utils.js';
import { decoders, encoders } from '#app/internal/transport';
import * as e from '../messages/shared-errors.js';
import * as w from '../messages/shared-warnings.js';
/**
* @param {string} route_id
* @param {string} dep
*/
export function validate_depends(route_id, dep) {
const match = /^(moz-icon|view-source|jar):/.exec(dep);
if (match) {
w.depends_special_scheme({ route: route_id, dependency: dep, scheme: match[1] });
}
}
/**
* Same-origin urls are keyed by path, so prerendered pages can be served from any origin
* @param {URL} url
* @param {{ origin: string }} page
*/
export function fetch_cache_url(url, page) {
return url.origin === page.origin ? url.href.slice(page.origin.length) : url.href;
}
export const INVALIDATED_PARAM = 'x-sveltekit-invalidated';
export const TRAILING_SLASH_PARAM = 'x-sveltekit-trailing-slash';
/**
* @param {any} data
* @param {string} [location_description]
*/
export function validate_load_response(data, location_description) {
if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
e.load_invalid_response({
location: String(location_description),
type:
typeof data !== 'object'
? `a ${typeof data}`
: data instanceof Response
? 'a Response object'
: Array.isArray(data)
? 'an array'
: 'a non-plain object'
});
}
}
const object_proto_names = /* @__PURE__ */ Object.getOwnPropertyNames(Object.prototype)
.sort()
.join('\0');
/**
* @param {unknown} thing
* @returns {thing is Record<PropertyKey, unknown>}
*/
function is_plain_object(thing) {
if (typeof thing !== 'object' || thing === null) return false;
const proto = Object.getPrototypeOf(thing);
return (
proto === Object.prototype ||
proto === null ||
Object.getPrototypeOf(proto) === null ||
Object.getOwnPropertyNames(proto).sort().join('\0') === object_proto_names
);
}
/**
* @param {Record<string, any>} value
* @param {Map<object, any>} clones
*/
function to_sorted(value, clones) {
const clone = Object.getPrototypeOf(value) === null ? Object.create(null) : {};
clones.set(value, clone);
Object.defineProperty(clone, remote_arg_marker, { value: true });
for (const key of Object.keys(value).sort()) {
const property = value[key];
Object.defineProperty(clone, key, {
value: clones.get(property) ?? property,
enumerable: true,
configurable: true,
writable: true
});
}
return clone;
}
// "sveltekit remote arg"
const remote_object = '__skrao';
const remote_map = '__skram';
const remote_set = '__skras';
const remote_file = '__skraf';
const remote_promise_guard = '__skrap';
const remote_regex_guard = '__skrag';
const remote_arg_marker = Symbol(remote_object);
/**
* @param {boolean} sort
*/
function create_remote_arg_reducers(sort) {
/** @type {Record<string, (value: unknown) => unknown>} */
const remote_fns_reducers = {
/** @param {unknown} value */
[remote_regex_guard]: (value) => {
if (value instanceof RegExp) {
e.remote_argument_unsupported({ type: 'Regular expressions' });
}
}
};
if (sort) {
const clones = new Map();
/** @type {(value: unknown) => Array<[unknown, unknown]> | undefined} */
remote_fns_reducers[remote_map] = (value) => {
if (!(value instanceof Map)) {
return;
}
/** @type {Array<[string, string]>} */
const entries = [];
for (const [key, val] of value) {
entries.push([stringify(key), stringify(val)]);
}
return entries.sort(([a1, a2], [b1, b2]) => {
if (a1 < b1) return -1;
if (a1 > b1) return 1;
if (a2 < b2) return -1;
if (a2 > b2) return 1;
return 0;
});
};
/** @type {(value: unknown) => unknown[] | undefined} */
remote_fns_reducers[remote_set] = (value) => {
if (!(value instanceof Set)) {
return;
}
/** @type {string[]} */
const items = [];
for (const item of value) {
items.push(stringify(item));
}
items.sort();
return items;
};
/** @type {(value: unknown) => Record<PropertyKey, unknown> | undefined} */
remote_fns_reducers[remote_object] = (value) => {
if (!is_plain_object(value)) {
return;
}
if (Object.hasOwn(value, remote_arg_marker)) {
return;
}
if (clones.has(value)) {
return clones.get(value);
}
return to_sorted(value, clones);
};
}
const all_reducers = { ...encoders, ...remote_fns_reducers };
/** @type {(value: unknown) => string} */
const stringify = (value) => devalue.stringify(value, all_reducers);
return all_reducers;
}
function create_remote_arg_revivers() {
const remote_fns_revivers = {
/** @type {(value: unknown) => unknown} */
[remote_object]: (value) => value,
/** @type {(value: unknown) => Map<unknown, unknown>} */
[remote_map]: (value) => {
if (!Array.isArray(value)) {
throw new Error('Invalid data for Map reviver');
}
const map = new Map();
for (const item of value) {
if (
!Array.isArray(item) ||
item.length !== 2 ||
typeof item[0] !== 'string' ||
typeof item[1] !== 'string'
) {
throw new Error('Invalid data for Map reviver');
}
const [key, val] = item;
map.set(parse(key), parse(val));
}
return map;
},
/** @type {(value: unknown) => Set<unknown>} */
[remote_set]: (value) => {
if (!Array.isArray(value)) {
throw new Error('Invalid data for Set reviver');
}
const set = new Set();
for (const item of value) {
if (typeof item !== 'string') {
throw new Error('Invalid data for Set reviver');
}
set.add(parse(item));
}
return set;
},
/** @type {(value: any) => File} */
[remote_file]: (value) => {
if (
!value ||
typeof value !== 'object' ||
typeof value.name !== 'string' ||
typeof value.type !== 'string' ||
typeof value.size !== 'number' ||
typeof value.lastModified !== 'number' ||
!(value.data instanceof ArrayBuffer)
) {
throw new Error('Invalid data for File reviver');
}
const { data, name, ...meta } = value;
return new File([data], name, meta);
}
};
const all_revivers = { ...decoders, ...remote_fns_revivers };
/** @type {(data: string) => unknown} */
const parse = (data) => devalue.parse(data, all_revivers);
return all_revivers;
}
/**
* Stringifies the argument (if any) for a remote function in such a way that
* it is both a valid URL and a valid file name (necessary for prerendering).
* @param {any} value
*/
export function stringify_remote_arg(value) {
if (value === undefined) return '';
// If people hit file/url size limits, we can look into using something like compress_and_encode_text from svelte.dev beyond a certain size
const json = devalue.stringify(value, create_remote_arg_reducers(true));
return url_friendly_base64_encode(json);
}
/**
* Stringifies command arguments, including `File` objects.
* @param {any} value
*/
export async function stringify_command_arg(value) {
if (value === undefined) return '';
const reducers = create_remote_arg_reducers(false);
/** @type {Set<Promise<any>>} */
const allowed_promises = new Set();
/** @param {any} value */
reducers[remote_file] = (value) => {
if (value instanceof File) {
const promise = value.arrayBuffer().then((data) => ({
data,
lastModified: value.lastModified,
name: value.name,
size: value.size,
type: value.type
}));
allowed_promises.add(promise);
return promise;
}
};
// we don't want to allow arbitrary promises, because they won't
// show up as promises on the other side. this is something
// we could potentially change in future. stringifyAsync
// will await them, so we need to explicitly deny them
/** @param {unknown} value */
reducers[remote_promise_guard] = (value) => {
if (value instanceof Promise && !allowed_promises.has(value)) {
e.remote_argument_unsupported({ type: 'Promises' });
}
};
const json = await devalue.stringifyAsync(value, reducers);
return url_friendly_base64_encode(json);
}
/**
* Base64-encodes `string` in such a way that the result is safe to use
* as both a URI component and a filename
* @param {string} string
*/
function url_friendly_base64_encode(string) {
const bytes = text_encoder.encode(string);
// TODO replace with `bytes.toBase64({ alphabet: 'base64url', omitPadding: true })` when we require Node >= 25
return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_');
}
/**
* Parses the argument (if any) for a remote function
* @param {string} string
*/
export function parse_remote_arg(string) {
if (!string) return undefined;
const json_string = text_decoder.decode(
// TODO replace with `Uint8Array.fromBase64(string, { alphabet: 'base64url' })` when we require Node >= 25
// no need to add back `=` characters, atob can handle it
base64_decode(string.replaceAll('-', '+').replaceAll('_', '/'))
);
return devalue.parse(json_string, create_remote_arg_revivers());
}
/**
* @param {string} id
* @param {string} payload
*/
export function create_remote_key(id, payload) {
return id + '/' + payload;
}
/**
* @param {string} key
* @returns {{ id: string; payload: string }}
*/
export function split_remote_key(key) {
const i = key.lastIndexOf('/');
if (i === -1) {
throw new Error(`Invalid remote key: ${key}`);
}
return {
id: key.slice(0, i),
payload: key.slice(i + 1)
};
}