Initial commit

This commit is contained in:
2026-01-18 21:01:43 +03:00
commit 455b6df2ba
3329 changed files with 1143638 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
/**
* Removes nullish values from an array.
*
* @template T
* @param {Array<T>} arr
*/
export function compact(arr) {
return arr.filter(/** @returns {val is NonNullable<T>} */ (val) => val != null);
}
+33
View File
@@ -0,0 +1,33 @@
/**
* @param {Record<string, string>} env
* @param {{
* public_prefix: string;
* private_prefix: string;
* }} prefixes
* @returns {Record<string, string>}
*/
export function filter_private_env(env, { public_prefix, private_prefix }) {
return Object.fromEntries(
Object.entries(env).filter(
([k]) =>
k.startsWith(private_prefix) && (public_prefix === '' || !k.startsWith(public_prefix))
)
);
}
/**
* @param {Record<string, string>} env
* @param {{
* public_prefix: string;
* private_prefix: string;
* }} prefixes
* @returns {Record<string, string>}
*/
export function filter_public_env(env, { public_prefix, private_prefix }) {
return Object.fromEntries(
Object.entries(env).filter(
([k]) =>
k.startsWith(public_prefix) && (private_prefix === '' || !k.startsWith(private_prefix))
)
);
}
+22
View File
@@ -0,0 +1,22 @@
/**
* @param {unknown} err
* @return {Error}
*/
export function coalesce_to_error(err) {
return err instanceof Error ||
(err && /** @type {any} */ (err).name && /** @type {any} */ (err).message)
? /** @type {Error} */ (err)
: new Error(JSON.stringify(err));
}
/**
* This is an identity function that exists to make TypeScript less
* paranoid about people throwing things that aren't errors, which
* frankly is not something we should care about
* @param {unknown} error
*/
export function normalize_error(error) {
return /** @type {import('../runtime/control.js').Redirect | import('../runtime/control.js').HttpError | Error} */ (
error
);
}
+46
View File
@@ -0,0 +1,46 @@
/**
* When inside a double-quoted attribute value, only `&` and `"` hold special meaning.
* @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
* @type {Record<string, string>}
*/
const escape_html_attr_dict = {
'&': '&amp;',
'"': '&quot;'
};
const escape_html_attr_regex = new RegExp(
// special characters
`[${Object.keys(escape_html_attr_dict).join('')}]|` +
// high surrogate without paired low surrogate
'[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
// a valid surrogate pair, the only match with 2 code units
// we match it so that we can match unpaired low surrogates in the same pass
// TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
'[\\ud800-\\udbff][\\udc00-\\udfff]|' +
// unpaired low surrogate (see previous match)
'[\\udc00-\\udfff]',
'g'
);
/**
* Formats a string to be used as an attribute's value in raw HTML.
*
* It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes
* characters that are special in attributes, and surrounds the whole string in double-quotes.
*
* @param {string} str
* @returns {string} Escaped string surrounded by double-quotes.
* @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`;
*/
export function escape_html_attr(str) {
const escaped_str = str.replace(escape_html_attr_regex, (match) => {
if (match.length === 2) {
// valid surrogate pair
return match;
}
return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`;
});
return `"${escaped_str}"`;
}
+93
View File
@@ -0,0 +1,93 @@
/**
* @param {Set<string>} expected
*/
function validator(expected) {
/**
* @param {any} module
* @param {string} [file]
*/
function validate(module, file) {
if (!module) return;
for (const key in module) {
if (key[0] === '_' || expected.has(key)) continue; // key is valid in this module
const values = [...expected.values()];
const hint =
hint_for_supported_files(key, file?.slice(file.lastIndexOf('.'))) ??
`valid exports are ${values.join(', ')}, or anything with a '_' prefix`;
throw new Error(`Invalid export '${key}'${file ? ` in ${file}` : ''} (${hint})`);
}
}
return validate;
}
/**
* @param {string} key
* @param {string} ext
* @returns {string | void}
*/
function hint_for_supported_files(key, ext = '.js') {
const supported_files = [];
if (valid_layout_exports.has(key)) {
supported_files.push(`+layout${ext}`);
}
if (valid_page_exports.has(key)) {
supported_files.push(`+page${ext}`);
}
if (valid_layout_server_exports.has(key)) {
supported_files.push(`+layout.server${ext}`);
}
if (valid_page_server_exports.has(key)) {
supported_files.push(`+page.server${ext}`);
}
if (valid_server_exports.has(key)) {
supported_files.push(`+server${ext}`);
}
if (supported_files.length > 0) {
return `'${key}' is a valid export in ${supported_files.slice(0, -1).join(', ')}${
supported_files.length > 1 ? ' or ' : ''
}${supported_files.at(-1)}`;
}
}
const valid_layout_exports = new Set([
'load',
'prerender',
'csr',
'ssr',
'trailingSlash',
'config'
]);
const valid_page_exports = new Set([...valid_layout_exports, 'entries']);
const valid_layout_server_exports = new Set([...valid_layout_exports]);
const valid_page_server_exports = new Set([...valid_layout_server_exports, 'actions', 'entries']);
const valid_server_exports = new Set([
'GET',
'POST',
'PATCH',
'PUT',
'DELETE',
'OPTIONS',
'HEAD',
'fallback',
'prerender',
'trailingSlash',
'config',
'entries'
]);
export const validate_layout_exports = validator(valid_layout_exports);
export const validate_page_exports = validator(valid_page_exports);
export const validate_layout_server_exports = validator(valid_layout_server_exports);
export const validate_page_server_exports = validator(valid_page_server_exports);
export const validate_server_exports = validator(valid_server_exports);
+183
View File
@@ -0,0 +1,183 @@
import fs from 'node:fs';
import path from 'node:path';
/** @param {string} dir */
export function mkdirp(dir) {
try {
fs.mkdirSync(dir, { recursive: true });
} catch (/** @type {any} */ e) {
if (e.code === 'EEXIST') {
if (!fs.statSync(dir).isDirectory()) {
throw new Error(`Cannot create directory ${dir}, a file already exists at this position`);
}
return;
}
throw e;
}
}
/** @param {string} path */
export function rimraf(path) {
fs.rmSync(path, { force: true, recursive: true });
}
/**
* @param {string} source
* @param {string} target
* @param {{
* filter?: (basename: string) => boolean;
* replace?: Record<string, string>;
* }} opts
*/
export function copy(source, target, opts = {}) {
if (!fs.existsSync(source)) return [];
/** @type {string[]} */
const files = [];
const prefix = posixify(target) + '/';
const regex = opts.replace
? new RegExp(`\\b(${Object.keys(opts.replace).join('|')})\\b`, 'g')
: null;
/**
* @param {string} from
* @param {string} to
*/
function go(from, to) {
if (opts.filter && !opts.filter(path.basename(from))) return;
const stats = fs.statSync(from);
if (stats.isDirectory()) {
fs.readdirSync(from).forEach((file) => {
go(path.join(from, file), path.join(to, file));
});
} else {
mkdirp(path.dirname(to));
if (opts.replace) {
const data = fs.readFileSync(from, 'utf-8');
fs.writeFileSync(
to,
data.replace(
/** @type {RegExp} */ (regex),
(_match, key) => /** @type {Record<string, string>} */ (opts.replace)[key]
)
);
} else {
fs.copyFileSync(from, to);
}
files.push(to === target ? posixify(path.basename(to)) : posixify(to).replace(prefix, ''));
}
}
go(source, target);
return files;
}
/**
* Get a list of all files in a directory
* @param {string} cwd - the directory to walk
* @param {boolean} [dirs] - whether to include directories in the result
* @returns {string[]} a list of all found files (and possibly directories) relative to `cwd`
*/
export function walk(cwd, dirs = false) {
/** @type {string[]} */
const all_files = [];
/** @param {string} dir */
function walk_dir(dir) {
const files = fs.readdirSync(path.join(cwd, dir));
for (const file of files) {
const joined = path.join(dir, file);
const stats = fs.statSync(path.join(cwd, joined));
if (stats.isDirectory()) {
if (dirs) all_files.push(joined);
walk_dir(joined);
} else {
all_files.push(joined);
}
}
}
return walk_dir(''), all_files;
}
/** @param {string} str */
export function posixify(str) {
return str.replace(/\\/g, '/');
}
/**
* Like `path.join`, but posixified and with a leading `./` if necessary
* @param {string[]} str
*/
export function join_relative(...str) {
let result = posixify(path.join(...str));
if (!result.startsWith('.')) {
result = `./${result}`;
}
return result;
}
/**
* Like `path.relative`, but always posixified and with a leading `./` if necessary.
* Useful for JS imports so the path can safely reside inside of `node_modules`.
* Otherwise paths could be falsely interpreted as package paths.
* @param {string} from
* @param {string} to
*/
export function relative_path(from, to) {
return join_relative(path.relative(from, to));
}
/**
* Prepend given path with `/@fs` prefix
* @param {string} str
*/
export function to_fs(str) {
str = posixify(str);
return `/@fs${
// Windows/Linux separation - Windows starts with a drive letter, we need a / in front there
str.startsWith('/') ? '' : '/'
}${str}`;
}
/**
* Given an entry point like [cwd]/src/hooks, returns a filename like [cwd]/src/hooks.js or [cwd]/src/hooks/index.js
* @param {string} entry
* @returns {string|null}
*/
export function resolve_entry(entry) {
if (fs.existsSync(entry)) {
const stats = fs.statSync(entry);
if (stats.isDirectory()) {
return resolve_entry(path.join(entry, 'index'));
}
return entry;
} else {
const dir = path.dirname(entry);
if (fs.existsSync(dir)) {
const base = path.basename(entry);
const files = fs.readdirSync(dir);
const found = files.find((file) => file.replace(/\.[^.]+$/, '') === base);
if (found) return path.join(dir, found);
}
}
return null;
}
/** @param {string} file */
export function read(file) {
return fs.readFileSync(file, 'utf-8');
}
+69
View File
@@ -0,0 +1,69 @@
import { fileURLToPath } from 'node:url';
import { Worker, parentPort } from 'node:worker_threads';
/**
* Runs a task in a subprocess so any dangling stuff gets killed upon completion.
* The subprocess needs to be the file `forked` is called in, and `forked` needs to be called eagerly at the top level.
* @template T
* @template U
* @param {string} module `import.meta.url` of the file
* @param {(opts: T) => U} callback The function that is invoked in the subprocess
* @returns {(opts: T) => Promise<U>} A function that when called starts the subprocess
*/
export function forked(module, callback) {
if (process.env.SVELTEKIT_FORK && parentPort) {
parentPort.on(
'message',
/** @param {any} data */ async (data) => {
if (data?.type === 'args' && data.module === module) {
parentPort?.postMessage({
type: 'result',
module,
payload: await callback(data.payload)
});
}
}
);
parentPort.postMessage({ type: 'ready', module });
}
/**
* @param {T} opts
* @returns {Promise<U>}
*/
return function (opts) {
return new Promise((fulfil, reject) => {
const worker = new Worker(fileURLToPath(module), {
env: {
...process.env,
SVELTEKIT_FORK: 'true'
}
});
worker.on(
'message',
/** @param {any} data */ (data) => {
if (data?.type === 'ready' && data.module === module) {
worker.postMessage({
type: 'args',
module,
payload: opts
});
}
if (data?.type === 'result' && data.module === module) {
worker.unref();
fulfil(data.payload);
}
}
);
worker.on('exit', (code) => {
if (code) {
reject(new Error(`Failed with code ${code}`));
}
});
});
};
}
+16
View File
@@ -0,0 +1,16 @@
/**
* @template T
* @param {() => T} fn
*/
export function once(fn) {
let done = false;
/** @type T */
let result;
return () => {
if (done) return result;
done = true;
return (result = fn());
};
}
+79
View File
@@ -0,0 +1,79 @@
/**
* Given an Accept header and a list of possible content types, pick
* the most suitable one to respond with
* @param {string} accept
* @param {string[]} types
*/
export function negotiate(accept, types) {
/** @type {Array<{ type: string, subtype: string, q: number, i: number }>} */
const parts = [];
accept.split(',').forEach((str, i) => {
const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str);
// no match equals invalid header — ignore
if (match) {
const [, type, subtype, q = '1'] = match;
parts.push({ type, subtype, q: +q, i });
}
});
parts.sort((a, b) => {
if (a.q !== b.q) {
return b.q - a.q;
}
if ((a.subtype === '*') !== (b.subtype === '*')) {
return a.subtype === '*' ? 1 : -1;
}
if ((a.type === '*') !== (b.type === '*')) {
return a.type === '*' ? 1 : -1;
}
return a.i - b.i;
});
let accepted;
let min_priority = Infinity;
for (const mimetype of types) {
const [type, subtype] = mimetype.split('/');
const priority = parts.findIndex(
(part) =>
(part.type === type || part.type === '*') &&
(part.subtype === subtype || part.subtype === '*')
);
if (priority !== -1 && priority < min_priority) {
accepted = mimetype;
min_priority = priority;
}
}
return accepted;
}
/**
* Returns `true` if the request contains a `content-type` header with the given type
* @param {Request} request
* @param {...string} types
*/
function is_content_type(request, ...types) {
const type = request.headers.get('content-type')?.split(';', 1)[0].trim() ?? '';
return types.includes(type.toLowerCase());
}
/**
* @param {Request} request
*/
export function is_form_content_type(request) {
// These content types must be protected against CSRF
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype
return is_content_type(
request,
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain'
);
}
+1
View File
@@ -0,0 +1 @@
export const s = JSON.stringify;
+16
View File
@@ -0,0 +1,16 @@
/**
* @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash' | 'entries'} Option
* @template {(import('types').SSRNode['universal'] | import('types').SSRNode['server'])[Option]} Value
*
* @param {Array<import('types').SSRNode | undefined>} nodes
* @param {Option} option
*
* @returns {Value | undefined}
*/
export function get_option(nodes, option) {
return nodes.reduce((value, node) => {
return /** @type {Value} TypeScript's too dumb to understand this */ (
node?.universal?.[option] ?? node?.server?.[option] ?? value
);
}, /** @type {Value | undefined} */ (undefined));
}
+1
View File
@@ -0,0 +1 @@
export const should_polyfill = typeof Deno === 'undefined' && typeof Bun === 'undefined';
+61
View File
@@ -0,0 +1,61 @@
import { DEV } from 'esm-env';
/** @type {Set<string> | null} */
let warned = null;
// TODO v2: remove all references to unwrap_promises
/**
* Given an object, return a new object where all top level values are awaited
*
* @param {Record<string, any>} object
* @param {string | null} [id]
* @returns {Promise<Record<string, any>>}
*/
export async function unwrap_promises(object, id) {
if (DEV) {
/** @type {string[]} */
const promises = [];
for (const key in object) {
if (typeof object[key]?.then === 'function') {
promises.push(key);
}
}
if (promises.length > 0) {
if (!warned) warned = new Set();
const last = promises.pop();
const properties =
promises.length > 0
? `${promises.map((p) => `"${p}"`).join(', ')} and "${last}" properties`
: `"${last}" property`;
const location = id ? `the \`load\` function in ${id}` : 'a `load` function';
const description = promises.length > 0 ? 'are promises' : 'is a promise';
const message = `The top-level ${properties} returned from ${location} ${description}.`;
if (!warned.has(message)) {
console.warn(
`\n${message}\n\nIn SvelteKit 2.0, these will no longer be awaited automatically. To get rid of this warning, await all promises included as top-level properties in \`load\` return values.\n`
);
warned.add(message);
}
}
}
for (const key in object) {
if (typeof object[key]?.then === 'function') {
return Object.fromEntries(
await Promise.all(Object.entries(object).map(async ([key, value]) => [key, await value]))
);
}
}
return object;
}
+216
View File
@@ -0,0 +1,216 @@
const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
/**
* Creates the regex pattern, extracts parameter names, and generates types for a route
* @param {string} id
*/
export function parse_route_id(id) {
/** @type {import('types').RouteParam[]} */
const params = [];
const pattern =
id === '/'
? /^\/$/
: new RegExp(
`^${get_route_segments(id)
.map((segment) => {
// special case — /[...rest]/ could contain zero segments
const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
if (rest_match) {
params.push({
name: rest_match[1],
matcher: rest_match[2],
optional: false,
rest: true,
chained: true
});
return '(?:/(.*))?';
}
// special case — /[[optional]]/ could contain zero segments
const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(segment);
if (optional_match) {
params.push({
name: optional_match[1],
matcher: optional_match[2],
optional: true,
rest: false,
chained: true
});
return '(?:/([^/]+))?';
}
if (!segment) {
return;
}
const parts = segment.split(/\[(.+?)\](?!\])/);
const result = parts
.map((content, i) => {
if (i % 2) {
if (content.startsWith('x+')) {
return escape(String.fromCharCode(parseInt(content.slice(2), 16)));
}
if (content.startsWith('u+')) {
return escape(
String.fromCharCode(
...content
.slice(2)
.split('-')
.map((code) => parseInt(code, 16))
)
);
}
const match = param_pattern.exec(content);
if (!match) {
throw new Error(
`Invalid param: ${content}. Params and matcher names can only have underscores and alphanumeric characters.`
);
}
const [, is_optional, is_rest, name, matcher] = match;
// It's assumed that the following invalid route id cases are already checked
// - unbalanced brackets
// - optional param following rest param
params.push({
name,
matcher,
optional: !!is_optional,
rest: !!is_rest,
chained: is_rest ? i === 1 && parts[0] === '' : false
});
return is_rest ? '(.*?)' : is_optional ? '([^/]*)?' : '([^/]+?)';
}
return escape(content);
})
.join('');
return '/' + result;
})
.join('')}/?$`
);
return { pattern, params };
}
const optional_param_regex = /\/\[\[\w+?(?:=\w+)?\]\]/;
/**
* Removes optional params from a route ID.
* @param {string} id
* @returns The route id with optional params removed
*/
export function remove_optional_params(id) {
return id.replace(optional_param_regex, '');
}
/**
* Returns `false` for `(group)` segments
* @param {string} segment
*/
function affects_path(segment) {
return !/^\([^)]+\)$/.test(segment);
}
/**
* Splits a route id into its segments, removing segments that
* don't affect the path (i.e. groups). The root route is represented by `/`
* and will be returned as `['']`.
* @param {string} route
* @returns string[]
*/
export function get_route_segments(route) {
return route.slice(1).split('/').filter(affects_path);
}
/**
* @param {RegExpMatchArray} match
* @param {import('types').RouteParam[]} params
* @param {Record<string, import('@sveltejs/kit').ParamMatcher>} matchers
*/
export function exec(match, params, matchers) {
/** @type {Record<string, string>} */
const result = {};
const values = match.slice(1);
const values_needing_match = values.filter((value) => value !== undefined);
let buffered = 0;
for (let i = 0; i < params.length; i += 1) {
const param = params[i];
let value = values[i - buffered];
// in the `[[a=b]]/.../[...rest]` case, if one or more optional parameters
// weren't matched, roll the skipped values into the rest
if (param.chained && param.rest && buffered) {
value = values
.slice(i - buffered, i + 1)
.filter((s) => s)
.join('/');
buffered = 0;
}
// if `value` is undefined, it means this is an optional or rest parameter
if (value === undefined) {
if (param.rest) result[param.name] = '';
continue;
}
if (!param.matcher || matchers[param.matcher](value)) {
result[param.name] = value;
// Now that the params match, reset the buffer if the next param isn't the [...rest]
// and the next value is defined, otherwise the buffer will cause us to skip values
const next_param = params[i + 1];
const next_value = values[i + 1];
if (next_param && !next_param.rest && next_param.optional && next_value && param.chained) {
buffered = 0;
}
// There are no more params and no more values, but all non-empty values have been matched
if (
!next_param &&
!next_value &&
Object.keys(result).length === values_needing_match.length
) {
buffered = 0;
}
continue;
}
// in the `/[[a=b]]/...` case, if the value didn't satisfy the matcher,
// keep track of the number of skipped optional parameters and continue
if (param.optional && param.chained) {
buffered++;
continue;
}
// otherwise, if the matcher returns `false`, the route did not match
return;
}
if (buffered) return;
return result;
}
/** @param {string} str */
function escape(str) {
return (
str
.normalize()
// escape [ and ] before escaping other characters, since they are used in the replacements
.replace(/[[\]]/g, '\\$&')
// replace %, /, ? and # with their encoded versions because decode_pathname leaves them untouched
.replace(/%/g, '%25')
.replace(/\//g, '%2[Ff]')
.replace(/\?/g, '%3[Ff]')
.replace(/#/g, '%23')
// escape characters that have special meaning in regex
.replace(/[.*+?^${}()|\\]/g, '\\$&')
);
}
+51
View File
@@ -0,0 +1,51 @@
/**
* @returns {import('types').Deferred & { promise: Promise<any> }}}
*/
function defer() {
let fulfil;
let reject;
const promise = new Promise((f, r) => {
fulfil = f;
reject = r;
});
// @ts-expect-error
return { promise, fulfil, reject };
}
/**
* Create an async iterator and a function to push values into it
* @returns {{
* iterator: AsyncIterable<any>;
* push: (value: any) => void;
* done: () => void;
* }}
*/
export function create_async_iterator() {
const deferred = [defer()];
return {
iterator: {
[Symbol.asyncIterator]() {
return {
next: async () => {
const next = await deferred[0].promise;
if (!next.done) deferred.shift();
return next;
}
};
}
},
push: (value) => {
deferred[deferred.length - 1].fulfil({
value,
done: false
});
deferred.push(defer());
},
done: () => {
deferred[deferred.length - 1].fulfil({ done: true });
}
};
}
+202
View File
@@ -0,0 +1,202 @@
import { BROWSER } from 'esm-env';
/**
* Matches a URI scheme. See https://www.rfc-editor.org/rfc/rfc3986#section-3.1
* @type {RegExp}
*/
export const SCHEME = /^[a-z][a-z\d+\-.]+:/i;
const absolute = /^([a-z]+:)?\/?\//;
/**
* @param {string} base
* @param {string} path
*/
export function resolve(base, path) {
if (SCHEME.test(path)) return path;
if (path[0] === '#') return base + path;
const base_match = absolute.exec(base);
const path_match = absolute.exec(path);
if (!base_match) {
throw new Error(`bad base path: "${base}"`);
}
const baseparts = path_match ? [] : base.slice(base_match[0].length).split('/');
const pathparts = path_match ? path.slice(path_match[0].length).split('/') : path.split('/');
baseparts.pop();
for (let i = 0; i < pathparts.length; i += 1) {
const part = pathparts[i];
if (part === '.') continue;
else if (part === '..') baseparts.pop();
else baseparts.push(part);
}
const prefix = (path_match && path_match[0]) || (base_match && base_match[0]) || '';
return `${prefix}${baseparts.join('/')}`;
}
/** @param {string} path */
export function is_root_relative(path) {
return path[0] === '/' && path[1] !== '/';
}
/**
* @param {string} path
* @param {import('types').TrailingSlash} trailing_slash
*/
export function normalize_path(path, trailing_slash) {
if (path === '/' || trailing_slash === 'ignore') return path;
if (trailing_slash === 'never') {
return path.endsWith('/') ? path.slice(0, -1) : path;
} else if (trailing_slash === 'always' && !path.endsWith('/')) {
return path + '/';
}
return path;
}
/**
* Decode pathname excluding %25 to prevent further double decoding of params
* @param {string} pathname
*/
export function decode_pathname(pathname) {
return pathname.split('%25').map(decodeURI).join('%25');
}
/** @param {Record<string, string>} params */
export function decode_params(params) {
for (const key in params) {
// input has already been decoded by decodeURI
// now handle the rest
params[key] = decodeURIComponent(params[key]);
}
return params;
}
/**
* The error when a URL is malformed is not very helpful, so we augment it with the URI
* @param {string} uri
*/
export function decode_uri(uri) {
try {
return decodeURI(uri);
} catch (e) {
if (e instanceof Error) {
e.message = `Failed to decode URI: ${uri}\n` + e.message;
}
throw e;
}
}
/**
* URL properties that could change during the lifetime of the page,
* which excludes things like `origin`
*/
const tracked_url_properties = /** @type {const} */ ([
'href',
'pathname',
'search',
'searchParams',
'toString',
'toJSON'
]);
/**
* @param {URL} url
* @param {() => void} callback
*/
export function make_trackable(url, callback) {
const tracked = new URL(url);
for (const property of tracked_url_properties) {
Object.defineProperty(tracked, property, {
get() {
callback();
return url[property];
},
enumerable: true,
configurable: true
});
}
if (!BROWSER) {
// @ts-ignore
tracked[Symbol.for('nodejs.util.inspect.custom')] = (depth, opts, inspect) => {
return inspect(url, opts);
};
}
disable_hash(tracked);
return tracked;
}
/**
* Disallow access to `url.hash` on the server and in `load`
* @param {URL} url
*/
export function disable_hash(url) {
allow_nodejs_console_log(url);
Object.defineProperty(url, 'hash', {
get() {
throw new Error(
'Cannot access event.url.hash. Consider using `$page.url.hash` inside a component instead'
);
}
});
}
/**
* Disallow access to `url.search` and `url.searchParams` during prerendering
* @param {URL} url
*/
export function disable_search(url) {
allow_nodejs_console_log(url);
for (const property of ['search', 'searchParams']) {
Object.defineProperty(url, property, {
get() {
throw new Error(`Cannot access url.${property} on a page with prerendering enabled`);
}
});
}
}
/**
* Allow URL to be console logged, bypassing disabled properties.
* @param {URL} url
*/
function allow_nodejs_console_log(url) {
if (!BROWSER) {
// @ts-ignore
url[Symbol.for('nodejs.util.inspect.custom')] = (depth, opts, inspect) => {
return inspect(new URL(url), opts);
};
}
}
const DATA_SUFFIX = '/__data.json';
/** @param {string} pathname */
export function has_data_suffix(pathname) {
return pathname.endsWith(DATA_SUFFIX);
}
/** @param {string} pathname */
export function add_data_suffix(pathname) {
return pathname.replace(/\/$/, '') + DATA_SUFFIX;
}
/** @param {string} pathname */
export function strip_data_suffix(pathname) {
return pathname.slice(0, -DATA_SUFFIX.length);
}