mirror of
https://gitverse.ru/kpa39l/telepost.git
synced 2026-09-29 18:05:08 +00:00
Initial commit
This commit is contained in:
+9
@@ -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
@@ -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
@@ -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
@@ -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 = {
|
||||
'&': '&',
|
||||
'"': '"'
|
||||
};
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export const s = JSON.stringify;
|
||||
+16
@@ -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
@@ -0,0 +1 @@
|
||||
export const should_polyfill = typeof Deno === 'undefined' && typeof Bun === 'undefined';
|
||||
+61
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user