INIT
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);
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
import MagicString from 'magic-string';
|
||||
import * as svelte from 'svelte/compiler';
|
||||
|
||||
/** @typedef {ReturnType<typeof import('svelte/compiler').parseCss>['children']} StyleSheetChildren */
|
||||
|
||||
/** @typedef {{ property: string; value: string; start: number; end: number; type: 'Declaration' }} Declaration */
|
||||
|
||||
const parse = svelte.parseCss
|
||||
? svelte.parseCss
|
||||
: /** @param {string} css */
|
||||
(css) => {
|
||||
return /** @type {{ css: { children: StyleSheetChildren } }} */ (
|
||||
svelte.parse(`<style>${css}</style>`)
|
||||
).css;
|
||||
};
|
||||
|
||||
const SKIP_PARSING_REGEX = /url\(/i;
|
||||
|
||||
/** Capture a single url(...) so we can process them one at a time */
|
||||
const URL_FUNCTION_REGEX = /url\(\s*.*?\)/gi;
|
||||
|
||||
/** Captures the value inside a CSS url(...) */
|
||||
const URL_PARAMETER_REGEX = /url\(\s*(['"]?)(.*?)\1\s*\)/i;
|
||||
|
||||
/** Splits the URL if there's a query string or hash fragment */
|
||||
const HASH_OR_QUERY_REGEX = /[#?]/;
|
||||
|
||||
/**
|
||||
* Assets handled by Vite that are referenced in the stylesheet always start
|
||||
* with this prefix because Vite emits them into the same directory as the CSS file
|
||||
*/
|
||||
const VITE_ASSET_PREFIX = './';
|
||||
|
||||
const AST_OFFSET = '<style>'.length;
|
||||
|
||||
/**
|
||||
* We need to fix the asset URLs in the CSS before we inline them into a document
|
||||
* because they are now relative to the document instead of the CSS file.
|
||||
* @param {{
|
||||
* css: string;
|
||||
* vite_assets: Set<string>;
|
||||
* static_assets: Set<string>;
|
||||
* paths_assets: string;
|
||||
* base: string;
|
||||
* static_asset_prefix: string;
|
||||
* }} opts
|
||||
* @returns {string}
|
||||
*/
|
||||
export function fix_css_urls({
|
||||
css,
|
||||
vite_assets,
|
||||
static_assets,
|
||||
paths_assets,
|
||||
base,
|
||||
static_asset_prefix
|
||||
}) {
|
||||
// skip parsing if there are no url(...) occurrences
|
||||
if (!SKIP_PARSING_REGEX.test(css)) {
|
||||
return css;
|
||||
}
|
||||
|
||||
// safe guard in case of trailing slashes (but this should never happen)
|
||||
if (paths_assets.endsWith('/')) {
|
||||
paths_assets = paths_assets.slice(0, -1);
|
||||
}
|
||||
|
||||
if (base.endsWith('/')) {
|
||||
base = base.slice(0, -1);
|
||||
}
|
||||
|
||||
const s = new MagicString(css);
|
||||
|
||||
const parsed = parse(css);
|
||||
|
||||
for (const child of parsed.children) {
|
||||
find_declarations(child, (declaration) => {
|
||||
if (!SKIP_PARSING_REGEX.test) return;
|
||||
|
||||
const cleaned = tippex_comments_and_strings(declaration.value);
|
||||
|
||||
/** @type {string} */
|
||||
let new_value = declaration.value;
|
||||
|
||||
/** @type {RegExpExecArray | null} */
|
||||
let url_function_found;
|
||||
URL_FUNCTION_REGEX.lastIndex = 0;
|
||||
while ((url_function_found = URL_FUNCTION_REGEX.exec(cleaned))) {
|
||||
const [url_function] = url_function_found;
|
||||
|
||||
// After finding a legitimate url(...), we want to operate on the original
|
||||
// that may have a string inside it
|
||||
const original_url_function = declaration.value.slice(
|
||||
url_function_found.index,
|
||||
url_function_found.index + url_function.length
|
||||
);
|
||||
|
||||
const url_parameter_found = URL_PARAMETER_REGEX.exec(original_url_function);
|
||||
if (!url_parameter_found) continue;
|
||||
|
||||
const [, , url] = url_parameter_found;
|
||||
const [url_without_hash_or_query] = url.split(HASH_OR_QUERY_REGEX);
|
||||
|
||||
/** @type {string | undefined} */
|
||||
let new_prefix;
|
||||
|
||||
// Check if it's an asset processed by Vite...
|
||||
let current_prefix = url_without_hash_or_query.slice(0, VITE_ASSET_PREFIX.length);
|
||||
let filename = url_without_hash_or_query.slice(VITE_ASSET_PREFIX.length);
|
||||
const decoded = decodeURIComponent(filename);
|
||||
|
||||
if (current_prefix === VITE_ASSET_PREFIX && vite_assets.has(decoded)) {
|
||||
new_prefix = paths_assets;
|
||||
} else {
|
||||
// ...or if it's from the static directory
|
||||
current_prefix = url_without_hash_or_query.slice(0, static_asset_prefix.length);
|
||||
filename = url_without_hash_or_query.slice(static_asset_prefix.length);
|
||||
const decoded = decodeURIComponent(filename);
|
||||
|
||||
if (current_prefix === static_asset_prefix && static_assets.has(decoded)) {
|
||||
new_prefix = base;
|
||||
}
|
||||
}
|
||||
|
||||
if (!new_prefix) continue;
|
||||
|
||||
new_value = new_value.replace(`${current_prefix}${filename}`, `${new_prefix}/${filename}`);
|
||||
}
|
||||
|
||||
if (declaration.value === new_value) return;
|
||||
|
||||
if (!svelte.parseCss) {
|
||||
declaration.start = declaration.start - AST_OFFSET;
|
||||
declaration.end = declaration.end - AST_OFFSET;
|
||||
}
|
||||
|
||||
s.update(declaration.start, declaration.end, `${declaration.property}: ${new_value}`);
|
||||
});
|
||||
}
|
||||
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StyleSheetChildren[0]} rule
|
||||
* @param {(declaration: Declaration) => void} callback
|
||||
*/
|
||||
function find_declarations(rule, callback) {
|
||||
// Vite already inlines relative @import rules, so we don't need to handle them here
|
||||
if (!rule.block) return;
|
||||
|
||||
for (const child of rule.block.children) {
|
||||
if (child.type !== 'Declaration') {
|
||||
find_declarations(child, callback);
|
||||
continue;
|
||||
}
|
||||
callback(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces comment and string contents with whitespace.
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tippex_comments_and_strings(value) {
|
||||
let new_value = '';
|
||||
let escaped = false;
|
||||
let in_comment = false;
|
||||
|
||||
/** @type {null | '"' | "'"} */
|
||||
let quote_mark = null;
|
||||
|
||||
let i = 0;
|
||||
while (i < value.length) {
|
||||
const char = value[i];
|
||||
|
||||
if (in_comment) {
|
||||
if (char === '*' && value[i + 1] === '/') {
|
||||
in_comment = false;
|
||||
new_value += char;
|
||||
} else {
|
||||
new_value += ' ';
|
||||
}
|
||||
} else if (!quote_mark && char === '/' && value[i + 1] === '*') {
|
||||
in_comment = true;
|
||||
new_value += '/*';
|
||||
i++; // skip the '*' since we already added it
|
||||
} else if (escaped) {
|
||||
new_value += ' ';
|
||||
escaped = false;
|
||||
} else if (quote_mark && char === '\\') {
|
||||
escaped = true;
|
||||
new_value += ' ';
|
||||
} else if (char === quote_mark) {
|
||||
quote_mark = null;
|
||||
new_value += char;
|
||||
} else if (quote_mark) {
|
||||
new_value += ' ';
|
||||
} else if (quote_mark === null && (char === '"' || char === "'")) {
|
||||
quote_mark = char;
|
||||
new_value += char;
|
||||
} else {
|
||||
new_value += char;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return new_value;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @param {Record<string, string>} env
|
||||
* @param {string} allowed
|
||||
* @param {string} disallowed
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function filter_env(env, allowed, disallowed) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(env).filter(
|
||||
([k]) => k.startsWith(allowed) && (disallowed === '' || !k.startsWith(disallowed))
|
||||
)
|
||||
);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { HttpError, SvelteKitError } from '@sveltejs/kit/internal';
|
||||
|
||||
/**
|
||||
* @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('../exports/internal/index.js').Redirect | HttpError | SvelteKitError | Error} */ (
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} error
|
||||
*/
|
||||
export function get_status(error) {
|
||||
return error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} error
|
||||
*/
|
||||
export function get_message(error) {
|
||||
return error instanceof SvelteKitError ? error.text : 'Internal Error';
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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 = {
|
||||
'&': '&',
|
||||
'"': '"'
|
||||
// Svelte also escapes < because the escape function could be called inside a `noscript` there
|
||||
// https://github.com/sveltejs/svelte/security/advisories/GHSA-8266-84wp-wv5c
|
||||
// However, that doesn't apply in SvelteKit
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
const escape_html_dict = {
|
||||
'&': '&',
|
||||
'<': '<'
|
||||
};
|
||||
|
||||
const surrogates = // 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]';
|
||||
|
||||
const escape_html_attr_regex = new RegExp(
|
||||
`[${Object.keys(escape_html_attr_dict).join('')}]|` + surrogates,
|
||||
'g'
|
||||
);
|
||||
|
||||
const escape_html_regex = new RegExp(
|
||||
`[${Object.keys(escape_html_dict).join('')}]|` + surrogates,
|
||||
'g'
|
||||
);
|
||||
|
||||
/**
|
||||
* Escapes unpaired surrogates (which are allowed in js strings but invalid in HTML) and
|
||||
* escapes characters that are special.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {boolean} [is_attr]
|
||||
* @returns {string} escaped string
|
||||
* @example const html = `<tag data-value="${escape_html('value', true)}">...</tag>`;
|
||||
*/
|
||||
export function escape_html(str, is_attr) {
|
||||
const dict = is_attr ? escape_html_attr_dict : escape_html_dict;
|
||||
const escaped_str = str.replace(is_attr ? escape_html_attr_regex : escape_html_regex, (match) => {
|
||||
if (match.length === 2) {
|
||||
// valid surrogate pair
|
||||
return match;
|
||||
}
|
||||
|
||||
return 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);
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @param {string} route_id
|
||||
* @param {any} config
|
||||
* @param {string} feature
|
||||
* @param {import('@sveltejs/kit').Adapter | undefined} adapter
|
||||
*/
|
||||
export function check_feature(route_id, config, feature, adapter) {
|
||||
if (!adapter) return;
|
||||
|
||||
switch (feature) {
|
||||
case '$app/server:read': {
|
||||
const supported = adapter.supports?.read?.({
|
||||
route: { id: route_id },
|
||||
config
|
||||
});
|
||||
|
||||
if (!supported) {
|
||||
throw new Error(
|
||||
`Cannot use \`read\` from \`$app/server\` in ${route_id} when using ${adapter.name}. Please ensure that your adapter is up to date and supports this feature.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes `/@fs` prefix from given path and posixifies it
|
||||
* @param {string} str
|
||||
*/
|
||||
export function from_fs(str) {
|
||||
str = posixify(str);
|
||||
if (!str.startsWith('/@fs')) return str;
|
||||
|
||||
str = str.slice(4);
|
||||
// Windows/Linux separation - Windows starts with a drive letter, we need to strip the additional / here
|
||||
return str[2] === ':' && /[A-Z]/.test(str[1]) ? str.slice(1) : 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.isFile()) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
const index = path.join(entry, 'index');
|
||||
if (fs.existsSync(index + '.js') || fs.existsSync(index + '.ts')) {
|
||||
return resolve_entry(index);
|
||||
}
|
||||
}
|
||||
|
||||
const dir = path.dirname(entry);
|
||||
|
||||
if (fs.existsSync(dir)) {
|
||||
const base = path.basename(entry);
|
||||
const files = fs.readdirSync(dir);
|
||||
const found = files.find((file) => {
|
||||
return file.replace(/\.(js|ts)$/, '') === base && fs.statSync(path.join(dir, file)).isFile();
|
||||
});
|
||||
|
||||
if (found) return path.join(dir, found);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param {string} file */
|
||||
export function read(file) {
|
||||
return fs.readFileSync(file, 'utf-8');
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker, parentPort } from 'node:worker_threads';
|
||||
import process from 'node:process';
|
||||
|
||||
/**
|
||||
* 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) => Promise<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());
|
||||
};
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Hash using djb2
|
||||
* @param {import('types').StrictBody[]} values
|
||||
*/
|
||||
export function hash(...values) {
|
||||
let hash = 5381;
|
||||
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string') {
|
||||
let i = value.length;
|
||||
while (i) hash = (hash * 33) ^ value.charCodeAt(--i);
|
||||
} else if (ArrayBuffer.isView(value)) {
|
||||
const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
||||
let i = buffer.length;
|
||||
while (i) hash = (hash * 33) ^ buffer[--i];
|
||||
} else {
|
||||
throw new TypeError('value must be a string or TypedArray');
|
||||
}
|
||||
}
|
||||
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { BINARY_FORM_CONTENT_TYPE } from '../runtime/form-utils.js';
|
||||
|
||||
/**
|
||||
* 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 = /([^/ \t]+)\/([^; \t]+)[ \t]*(?:;[ \t]*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',
|
||||
BINARY_FORM_CONTENT_TYPE
|
||||
);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import process from 'node:process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Resolves a peer dependency relative to the current CWD. Duplicated with `packages/adapter-auto`
|
||||
* @param {string} dependency
|
||||
*/
|
||||
function resolve_peer(dependency) {
|
||||
let [name, ...parts] = dependency.split('/');
|
||||
if (name[0] === '@') name += `/${parts.shift()}`;
|
||||
|
||||
let dir = process.cwd();
|
||||
|
||||
while (!fs.existsSync(`${dir}/node_modules/${name}/package.json`)) {
|
||||
if (dir === (dir = path.dirname(dir))) {
|
||||
throw new Error(
|
||||
`Could not resolve peer dependency "${name}" relative to your project — please install it and try again.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const pkg_dir = `${dir}/node_modules/${name}`;
|
||||
const pkg = JSON.parse(fs.readFileSync(`${pkg_dir}/package.json`, 'utf-8'));
|
||||
|
||||
const subpackage = ['.', ...parts].join('/');
|
||||
|
||||
let exported = pkg.exports[subpackage];
|
||||
|
||||
while (typeof exported !== 'string') {
|
||||
if (!exported) {
|
||||
throw new Error(`Could not find valid "${subpackage}" export in ${name}/package.json`);
|
||||
}
|
||||
|
||||
exported = exported['import'] ?? exported['default'];
|
||||
}
|
||||
|
||||
return path.resolve(pkg_dir, exported);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a dependency relative to the current working directory,
|
||||
* rather than relative to this package (but falls back to trying that, if necessary)
|
||||
* @param {string} dependency
|
||||
*/
|
||||
export async function import_peer(dependency) {
|
||||
try {
|
||||
return await import(resolve_peer(dependency));
|
||||
} catch {
|
||||
return await import(dependency);
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const s = JSON.stringify;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
validate_layout_exports,
|
||||
validate_layout_server_exports,
|
||||
validate_page_exports,
|
||||
validate_page_server_exports
|
||||
} from './exports.js';
|
||||
|
||||
export class PageNodes {
|
||||
data;
|
||||
|
||||
/**
|
||||
* @param {Array<import('types').SSRNode | undefined>} nodes
|
||||
*/
|
||||
constructor(nodes) {
|
||||
this.data = nodes;
|
||||
}
|
||||
|
||||
layouts() {
|
||||
return this.data.slice(0, -1);
|
||||
}
|
||||
|
||||
page() {
|
||||
return this.data.at(-1);
|
||||
}
|
||||
|
||||
validate() {
|
||||
for (const layout of this.layouts()) {
|
||||
if (layout) {
|
||||
validate_layout_server_exports(layout.server, /** @type {string} */ (layout.server_id));
|
||||
validate_layout_exports(layout.universal, /** @type {string} */ (layout.universal_id));
|
||||
}
|
||||
}
|
||||
|
||||
const page = this.page();
|
||||
if (page) {
|
||||
validate_page_server_exports(page.server, /** @type {string} */ (page.server_id));
|
||||
validate_page_exports(page.universal, /** @type {string} */ (page.universal_id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {'prerender' | 'ssr' | 'csr' | 'trailingSlash'} Option
|
||||
* @param {Option} option
|
||||
* @returns {Value | undefined}
|
||||
*/
|
||||
#get_option(option) {
|
||||
/** @typedef {(import('types').UniversalNode | import('types').ServerNode)[Option]} Value */
|
||||
|
||||
return this.data.reduce((value, node) => {
|
||||
return node?.universal?.[option] ?? node?.server?.[option] ?? value;
|
||||
}, /** @type {Value | undefined} */ (undefined));
|
||||
}
|
||||
|
||||
csr() {
|
||||
return this.#get_option('csr') ?? true;
|
||||
}
|
||||
|
||||
ssr() {
|
||||
return this.#get_option('ssr') ?? true;
|
||||
}
|
||||
|
||||
prerender() {
|
||||
return this.#get_option('prerender') ?? false;
|
||||
}
|
||||
|
||||
trailing_slash() {
|
||||
return this.#get_option('trailingSlash') ?? 'never';
|
||||
}
|
||||
|
||||
get_config() {
|
||||
/** @type {any} */
|
||||
let current = {};
|
||||
|
||||
for (const node of this.data) {
|
||||
if (!node?.universal?.config && !node?.server?.config) continue;
|
||||
|
||||
current = {
|
||||
...current,
|
||||
// TODO: should we override the server config value with the universal value similar to other page options?
|
||||
...node?.universal?.config,
|
||||
...node?.server?.config
|
||||
};
|
||||
}
|
||||
|
||||
// TODO 3.0 always return `current`? then we can get rid of `?? {}` in other places
|
||||
return Object.keys(current).length ? current : undefined;
|
||||
}
|
||||
|
||||
should_prerender_data() {
|
||||
return this.data.some(
|
||||
// prerender in case of trailingSlash because the client retrieves that value from the server
|
||||
(node) => node?.server?.load || node?.server?.trailingSlash !== undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/** @see https://github.com/microsoft/TypeScript/blob/904e7dd97dc8da1352c8e05d70829dff17c73214/src/lib/es2024.promise.d.ts */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{
|
||||
* promise: Promise<T>;
|
||||
* resolve: (value: T | PromiseLike<T>) => void;
|
||||
* reject: (reason?: any) => void;
|
||||
* }} PromiseWithResolvers<T>
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO: Whenever Node >21 is minimum supported version, we can use `Promise.withResolvers` to avoid this ceremony
|
||||
*
|
||||
* @template T
|
||||
* @returns {PromiseWithResolvers<T>}
|
||||
*/
|
||||
export function with_resolvers() {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
// @ts-expect-error `resolve` and `reject` are assigned!
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
import { BROWSER } from 'esm-env';
|
||||
import { decode_params } from './url.js';
|
||||
|
||||
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))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// We know the match cannot be null in the browser because manifest generation
|
||||
// would have invoked this during build and failed if we hit an invalid
|
||||
// param/matcher name with non-alphanumeric character.
|
||||
const match = /** @type {RegExpExecArray} */ (param_pattern.exec(content));
|
||||
if (!BROWSER && !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 segment !== '' && !/^\([^)]+\)$/.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) {
|
||||
// We need to allow the matcher to run so that it can decide if this optional rest param should be allowed to match
|
||||
value = '';
|
||||
} else {
|
||||
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, '\\$&')
|
||||
);
|
||||
}
|
||||
|
||||
const basic_param_pattern = /\[(\[)?(\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
|
||||
|
||||
/**
|
||||
* Populate a route ID with params to resolve a pathname.
|
||||
* @example
|
||||
* ```js
|
||||
* resolveRoute(
|
||||
* `/blog/[slug]/[...somethingElse]`,
|
||||
* {
|
||||
* slug: 'hello-world',
|
||||
* somethingElse: 'something/else'
|
||||
* }
|
||||
* ); // `/blog/hello-world/something/else`
|
||||
* ```
|
||||
* @param {string} id
|
||||
* @param {Record<string, string | undefined>} params
|
||||
* @returns {string}
|
||||
*/
|
||||
export function resolve_route(id, params) {
|
||||
const segments = get_route_segments(id);
|
||||
const has_id_trailing_slash = id != '/' && id.endsWith('/');
|
||||
|
||||
return (
|
||||
'/' +
|
||||
segments
|
||||
.map((segment) =>
|
||||
segment.replace(basic_param_pattern, (_, optional, rest, name) => {
|
||||
const param_value = params[name];
|
||||
|
||||
// This is nested so TS correctly narrows the type
|
||||
if (!param_value) {
|
||||
if (optional) return '';
|
||||
if (rest && param_value !== undefined) return '';
|
||||
throw new Error(`Missing parameter '${name}' in route ${id}`);
|
||||
}
|
||||
|
||||
if (param_value.startsWith('/') || param_value.endsWith('/'))
|
||||
throw new Error(
|
||||
`Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar`
|
||||
);
|
||||
return param_value;
|
||||
})
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join('/') +
|
||||
(has_id_trailing_slash ? '/' : '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('types').SSRNode} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function has_server_load(node) {
|
||||
return node.server?.load !== undefined || node.server?.trailingSlash !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first route that matches the given path
|
||||
* @template {{pattern: RegExp, params: import('types').RouteParam[]}} Route
|
||||
* @param {string} path - The decoded pathname to match
|
||||
* @param {Route[]} routes
|
||||
* @param {Record<string, import('@sveltejs/kit').ParamMatcher>} matchers
|
||||
* @returns {{ route: Route, params: Record<string, string> } | null}
|
||||
*/
|
||||
export function find_route(path, routes, matchers) {
|
||||
for (const route of routes) {
|
||||
const match = route.pattern.exec(path);
|
||||
if (!match) continue;
|
||||
|
||||
const matched = exec(match, route.params, matchers);
|
||||
if (matched) {
|
||||
return {
|
||||
route,
|
||||
params: decode_params(matched)
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { with_resolvers } from './promise.js';
|
||||
|
||||
/**
|
||||
* Create an async iterator and a function to push values into it
|
||||
* @template T
|
||||
* @returns {{
|
||||
* iterate: (transform?: (input: T) => T) => AsyncIterable<T>;
|
||||
* add: (promise: Promise<T>) => void;
|
||||
* }}
|
||||
*/
|
||||
export function create_async_iterator() {
|
||||
let resolved = -1;
|
||||
let returned = -1;
|
||||
|
||||
/** @type {import('./promise.js').PromiseWithResolvers<T>[]} */
|
||||
const deferred = [];
|
||||
|
||||
return {
|
||||
iterate: (transform = (x) => x) => {
|
||||
return {
|
||||
[Symbol.asyncIterator]() {
|
||||
return {
|
||||
next: async () => {
|
||||
const next = deferred[++returned];
|
||||
if (!next) return { value: null, done: true };
|
||||
|
||||
const value = await next.promise;
|
||||
return { value: transform(value), done: false };
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
add: (promise) => {
|
||||
deferred.push(with_resolvers());
|
||||
void promise.then((value) => {
|
||||
deferred[++resolved].resolve(value);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import { BROWSER, DEV } 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 internal = new URL('sveltekit-internal://');
|
||||
|
||||
/**
|
||||
* @param {string} base
|
||||
* @param {string} path
|
||||
*/
|
||||
export function resolve(base, path) {
|
||||
// special case
|
||||
if (path[0] === '/' && path[1] === '/') return path;
|
||||
|
||||
let url = new URL(base, internal);
|
||||
url = new URL(path, url);
|
||||
|
||||
return url.protocol === internal.protocol ? url.pathname + url.search + url.hash : url.href;
|
||||
}
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns everything up to the first `#` in a URL
|
||||
* @param {{href: string}} url_like
|
||||
*/
|
||||
export function strip_hash({ href }) {
|
||||
return href.split('#')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {() => void} callback
|
||||
* @param {(search_param: string) => void} search_params_callback
|
||||
* @param {boolean} [allow_hash]
|
||||
*/
|
||||
export function make_trackable(url, callback, search_params_callback, allow_hash = false) {
|
||||
const tracked = new URL(url);
|
||||
|
||||
Object.defineProperty(tracked, 'searchParams', {
|
||||
value: new Proxy(tracked.searchParams, {
|
||||
get(obj, key) {
|
||||
if (key === 'get' || key === 'getAll' || key === 'has') {
|
||||
return (/** @type {string} */ param, /** @type {string[]} */ ...rest) => {
|
||||
search_params_callback(param);
|
||||
return obj[key](param, ...rest);
|
||||
};
|
||||
}
|
||||
|
||||
// if they try to access something different from what is in `tracked_search_params_properties`
|
||||
// we track the whole url (entries, values, keys etc)
|
||||
callback();
|
||||
|
||||
const value = Reflect.get(obj, key);
|
||||
return typeof value === 'function' ? value.bind(obj) : value;
|
||||
}
|
||||
}),
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
/**
|
||||
* URL properties that could change during the lifetime of the page,
|
||||
* which excludes things like `origin`
|
||||
*/
|
||||
const tracked_url_properties = ['href', 'pathname', 'search', 'toString', 'toJSON'];
|
||||
if (allow_hash) tracked_url_properties.push('hash');
|
||||
|
||||
for (const property of tracked_url_properties) {
|
||||
Object.defineProperty(tracked, property, {
|
||||
get() {
|
||||
callback();
|
||||
// @ts-expect-error
|
||||
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);
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
tracked.searchParams[Symbol.for('nodejs.util.inspect.custom')] = (depth, opts, inspect) => {
|
||||
return inspect(url.searchParams, opts);
|
||||
};
|
||||
}
|
||||
|
||||
if ((DEV || !BROWSER) && !allow_hash) {
|
||||
disable_hash(tracked);
|
||||
}
|
||||
|
||||
return tracked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow access to `url.hash` on the server and in `load`
|
||||
* @param {URL} url
|
||||
*/
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user