UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

377 lines (314 loc) • 9.91 kB
/** @import { Transport } from '@sveltejs/kit' */ import * as devalue from 'devalue'; import { base64_decode, base64_encode, text_encoder } from './utils.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) { console.warn( `${route_id}: Calling \`depends('${dep}')\` will throw an error in Firefox because \`${match[1]}\` is a special URI scheme` ); } } 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) { throw new Error( `a load function ${location_description} 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 {...}\`)` ); } } /** * Try to `devalue.stringify` the data object using the provided transport encoders. * @param {any} data * @param {Transport} transport */ export function stringify(data, transport) { const encoders = Object.fromEntries(Object.entries(transport).map(([k, v]) => [k, v.encode])); return devalue.stringify(data, encoders); } 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 {Transport} transport * @param {boolean} sort * @param {Map<any, any>} remote_arg_clones */ function create_remote_arg_reducers(transport, sort, remote_arg_clones) { /** @type {Record<string, (value: unknown) => unknown>} */ const remote_fns_reducers = { /** @param {unknown} value */ [remote_regex_guard]: (value) => { if (value instanceof RegExp) { throw new Error('Regular expressions are not valid remote function arguments'); } } }; if (sort) { /** @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 (remote_arg_clones.has(value)) { return remote_arg_clones.get(value); } return to_sorted(value, remote_arg_clones); }; } const user_reducers = Object.fromEntries( Object.entries(transport).map(([k, v]) => [k, v.encode]) ); const all_reducers = { ...user_reducers, ...remote_fns_reducers }; /** @type {(value: unknown) => string} */ const stringify = (value) => devalue.stringify(value, all_reducers); return all_reducers; } /** @param {Transport} transport */ function create_remote_arg_revivers(transport) { 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 user_revivers = Object.fromEntries( Object.entries(transport).map(([k, v]) => [k, v.decode]) ); const all_revivers = { ...user_revivers, ...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 * @param {Transport} transport */ export function stringify_remote_arg(value, transport) { 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(transport, true, new Map())); return url_friendly_base64_encode(json); } /** * Stringifies command arguments, including `File` objects. * @param {any} value * @param {Transport} transport */ export async function stringify_command_arg(value, transport) { if (value === undefined) return ''; const reducers = create_remote_arg_reducers(transport, false, new Map()); /** @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)) { throw new Error('Promises are not valid remote function arguments'); } }; 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); return base64_encode(bytes).replaceAll('=', '').replaceAll('+', '-').replaceAll('/', '_'); } /** * Parses the argument (if any) for a remote function * @param {string} string * @param {Transport} transport */ export function parse_remote_arg(string, transport) { if (!string) return undefined; const json_string = new TextDecoder().decode( // no need to add back `=` characters, atob can handle it base64_decode(string.replaceAll('-', '+').replaceAll('_', '/')) ); return devalue.parse(json_string, create_remote_arg_revivers(transport)); } /** * @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) }; }