344 lines
11 KiB
Plaintext
344 lines
11 KiB
Plaintext
let _tanstack_router_core_isServer = require("@tanstack/router-core/isServer");
|
|
//#region src/utils.ts
|
|
/**
|
|
* Return the last element of an array.
|
|
* Intended for non-empty arrays used within router internals.
|
|
*/
|
|
function last(arr) {
|
|
return arr[arr.length - 1];
|
|
}
|
|
function isFunction(d) {
|
|
return typeof d === "function";
|
|
}
|
|
/**
|
|
* Apply a value-or-updater to a previous value.
|
|
* Accepts either a literal value or a function of the previous value.
|
|
*/
|
|
function functionalUpdate(updater, previous) {
|
|
if (isFunction(updater)) return updater(previous);
|
|
return updater;
|
|
}
|
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
const isEnumerable = Object.prototype.propertyIsEnumerable;
|
|
function hasKeys(obj) {
|
|
for (const key in obj) if (hasOwn.call(obj, key)) return true;
|
|
return false;
|
|
}
|
|
const createNull = () => Object.create(null);
|
|
const nullReplaceEqualDeep = (prev, next) => replaceEqualDeep(prev, next, createNull);
|
|
/**
|
|
* This function returns `prev` if `_next` is deeply equal.
|
|
* If not, it will replace any deeply equal children of `b` with those of `a`.
|
|
* This can be used for structural sharing between immutable JSON values for example.
|
|
* Do not use this with signals
|
|
*/
|
|
function replaceEqualDeep(prev, _next, _makeObj = () => ({}), _depth = 0) {
|
|
if (_tanstack_router_core_isServer.isServer) return _next;
|
|
if (prev === _next) return prev;
|
|
if (_depth > 500) return _next;
|
|
const next = _next;
|
|
const array = isPlainArray(prev) && isPlainArray(next);
|
|
if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next;
|
|
const prevItems = array ? prev : getEnumerableOwnKeys(prev);
|
|
if (!prevItems) return next;
|
|
const nextItems = array ? next : getEnumerableOwnKeys(next);
|
|
if (!nextItems) return next;
|
|
const prevSize = prevItems.length;
|
|
const nextSize = nextItems.length;
|
|
const copy = array ? new Array(nextSize) : _makeObj();
|
|
let equalItems = 0;
|
|
for (let i = 0; i < nextSize; i++) {
|
|
const key = array ? i : nextItems[i];
|
|
const p = prev[key];
|
|
const n = next[key];
|
|
if (p === n) {
|
|
copy[key] = p;
|
|
if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++;
|
|
continue;
|
|
}
|
|
if (p === null || n === null || typeof p !== "object" || typeof n !== "object") {
|
|
copy[key] = n;
|
|
continue;
|
|
}
|
|
const v = replaceEqualDeep(p, n, _makeObj, _depth + 1);
|
|
copy[key] = v;
|
|
if (v === p) equalItems++;
|
|
}
|
|
return prevSize === nextSize && equalItems === prevSize ? prev : copy;
|
|
}
|
|
/**
|
|
* Equivalent to `Reflect.ownKeys`, but ensures that objects are "clone-friendly":
|
|
* will return false if object has any non-enumerable properties.
|
|
*
|
|
* Optimized for the common case where objects have no symbol properties.
|
|
*/
|
|
function getEnumerableOwnKeys(o) {
|
|
const names = Object.getOwnPropertyNames(o);
|
|
for (const name of names) if (!isEnumerable.call(o, name)) return false;
|
|
const symbols = Object.getOwnPropertySymbols(o);
|
|
if (symbols.length === 0) return names;
|
|
const keys = names;
|
|
for (const symbol of symbols) {
|
|
if (!isEnumerable.call(o, symbol)) return false;
|
|
keys.push(symbol);
|
|
}
|
|
return keys;
|
|
}
|
|
function isPlainObject(o) {
|
|
if (!hasObjectPrototype(o)) return false;
|
|
const ctor = o.constructor;
|
|
if (typeof ctor === "undefined") return true;
|
|
const prot = ctor.prototype;
|
|
if (!hasObjectPrototype(prot)) return false;
|
|
if (!prot.hasOwnProperty("isPrototypeOf")) return false;
|
|
return true;
|
|
}
|
|
function hasObjectPrototype(o) {
|
|
return Object.prototype.toString.call(o) === "[object Object]";
|
|
}
|
|
/**
|
|
* Check if a value is a "plain" array (no extra enumerable keys).
|
|
*/
|
|
function isPlainArray(value) {
|
|
return Array.isArray(value) && value.length === Object.keys(value).length;
|
|
}
|
|
/**
|
|
* Perform a deep equality check with options for partial comparison and
|
|
* ignoring `undefined` values. Optimized for router state comparisons.
|
|
*/
|
|
function deepEqual(a, b, opts) {
|
|
if (a === b) return true;
|
|
if (typeof a !== typeof b) return false;
|
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0, l = a.length; i < l; i++) if (!deepEqual(a[i], b[i], opts)) return false;
|
|
return true;
|
|
}
|
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
const ignoreUndefined = opts?.ignoreUndefined ?? true;
|
|
if (opts?.partial) {
|
|
for (const k in b) if (!ignoreUndefined || b[k] !== void 0) {
|
|
if (!deepEqual(a[k], b[k], opts)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
let aCount = 0;
|
|
if (!ignoreUndefined) aCount = Object.keys(a).length;
|
|
else for (const k in a) if (a[k] !== void 0) aCount++;
|
|
let bCount = 0;
|
|
for (const k in b) if (!ignoreUndefined || b[k] !== void 0) {
|
|
bCount++;
|
|
if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false;
|
|
}
|
|
return aCount === bCount;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Create a promise with exposed resolve/reject and status fields.
|
|
* Useful for coordinating async router lifecycle operations.
|
|
*/
|
|
function createControlledPromise(onResolve) {
|
|
let resolveLoadPromise;
|
|
let rejectLoadPromise;
|
|
const controlledPromise = new Promise((resolve, reject) => {
|
|
resolveLoadPromise = resolve;
|
|
rejectLoadPromise = reject;
|
|
});
|
|
controlledPromise.status = "pending";
|
|
controlledPromise.resolve = (value) => {
|
|
controlledPromise.status = "resolved";
|
|
controlledPromise.value = value;
|
|
resolveLoadPromise(value);
|
|
onResolve?.(value);
|
|
};
|
|
controlledPromise.reject = (e) => {
|
|
controlledPromise.status = "rejected";
|
|
rejectLoadPromise(e);
|
|
};
|
|
return controlledPromise;
|
|
}
|
|
/**
|
|
* Heuristically detect dynamic import "module not found" errors
|
|
* across major browsers for lazy route component handling.
|
|
*/
|
|
function isModuleNotFoundError(error) {
|
|
if (typeof error?.message !== "string") return false;
|
|
return error.message.startsWith("Failed to fetch dynamically imported module") || error.message.startsWith("error loading dynamically imported module") || error.message.startsWith("Importing a module script failed");
|
|
}
|
|
function isPromise(value) {
|
|
return Boolean(value && typeof value === "object" && typeof value.then === "function");
|
|
}
|
|
function findLast(array, predicate) {
|
|
for (let i = array.length - 1; i >= 0; i--) {
|
|
const item = array[i];
|
|
if (predicate(item)) return item;
|
|
}
|
|
}
|
|
/**
|
|
* Remove control characters that can cause open redirect vulnerabilities.
|
|
* Characters like \r (CR) and \n (LF) can trick URL parsers into interpreting
|
|
* paths like "/\r/evil.com" as "http://evil.com".
|
|
*/
|
|
function sanitizePathSegment(segment) {
|
|
return segment.replace(/[\x00-\x1f\x7f]/g, "");
|
|
}
|
|
function decodeSegment(segment) {
|
|
let decoded;
|
|
try {
|
|
decoded = decodeURI(segment);
|
|
} catch {
|
|
decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {
|
|
try {
|
|
return decodeURI(match);
|
|
} catch {
|
|
return match;
|
|
}
|
|
});
|
|
}
|
|
return sanitizePathSegment(decoded);
|
|
}
|
|
/**
|
|
* Default list of URL protocols to allow in links, redirects, and navigation.
|
|
* Any absolute URL protocol not in this list is treated as dangerous by default.
|
|
*/
|
|
const DEFAULT_PROTOCOL_ALLOWLIST = [
|
|
"http:",
|
|
"https:",
|
|
"mailto:",
|
|
"tel:"
|
|
];
|
|
/**
|
|
* Check if a URL string uses a protocol that is not in the allowlist.
|
|
* Returns true for blocked protocols like javascript:, blob:, data:, etc.
|
|
*
|
|
* The URL constructor correctly normalizes:
|
|
* - Mixed case (JavaScript: → javascript:)
|
|
* - Whitespace/control characters (java\nscript: → javascript:)
|
|
* - Leading whitespace
|
|
*
|
|
* For relative URLs (no protocol), returns false (safe).
|
|
*
|
|
* @param url - The URL string to check
|
|
* @param allowlist - Set of protocols to allow
|
|
* @returns true if the URL uses a protocol that is not allowed
|
|
*/
|
|
function isDangerousProtocol(url, allowlist) {
|
|
if (!url) return false;
|
|
try {
|
|
const parsed = new URL(url);
|
|
return !allowlist.has(parsed.protocol);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
const HTML_ESCAPE_LOOKUP = {
|
|
"&": "\\u0026",
|
|
">": "\\u003e",
|
|
"<": "\\u003c",
|
|
"\u2028": "\\u2028",
|
|
"\u2029": "\\u2029"
|
|
};
|
|
const HTML_ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
|
/**
|
|
* Escape HTML special characters in a string to prevent XSS attacks
|
|
* when embedding strings in script tags during SSR.
|
|
*
|
|
* This is essential for preventing XSS vulnerabilities when user-controlled
|
|
* content is embedded in inline scripts.
|
|
*/
|
|
function escapeHtml(str) {
|
|
return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]);
|
|
}
|
|
function decodePath(path) {
|
|
if (!path) return {
|
|
path,
|
|
handledProtocolRelativeURL: false
|
|
};
|
|
if (!/[%\\\x00-\x1f\x7f]/.test(path) && !path.startsWith("//")) return {
|
|
path,
|
|
handledProtocolRelativeURL: false
|
|
};
|
|
const re = /%25|%5C/gi;
|
|
let cursor = 0;
|
|
let result = "";
|
|
let match;
|
|
while (null !== (match = re.exec(path))) {
|
|
result += decodeSegment(path.slice(cursor, match.index)) + match[0];
|
|
cursor = re.lastIndex;
|
|
}
|
|
result = result + decodeSegment(cursor ? path.slice(cursor) : path);
|
|
let handledProtocolRelativeURL = false;
|
|
if (result.startsWith("//")) {
|
|
handledProtocolRelativeURL = true;
|
|
result = "/" + result.replace(/^\/+/, "");
|
|
}
|
|
return {
|
|
path: result,
|
|
handledProtocolRelativeURL
|
|
};
|
|
}
|
|
/**
|
|
* Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing.
|
|
*
|
|
* This function encodes:
|
|
* - Whitespace characters (spaces → %20, tabs → %09, etc.)
|
|
* - Non-ASCII/Unicode characters (emojis, accented characters, etc.)
|
|
*
|
|
* It preserves:
|
|
* - Already percent-encoded sequences (won't double-encode %2F, %25, etc.)
|
|
* - ASCII special characters valid in URL paths (@, $, &, +, etc.)
|
|
* - Forward slashes as path separators
|
|
*
|
|
* Used to generate proper href values for SSR without constructing URL objects.
|
|
*
|
|
* @example
|
|
* encodePathLikeUrl('/path/file name.pdf') // '/path/file%20name.pdf'
|
|
* encodePathLikeUrl('/path/日本語') // '/path/%E6%97%A5%E6%9C%AC%E8%AA%9E'
|
|
* encodePathLikeUrl('/path/already%20encoded') // '/path/already%20encoded' (preserved)
|
|
*/
|
|
function encodePathLikeUrl(path) {
|
|
if (!/\s|[^\u0000-\u007F]/.test(path)) return path;
|
|
return path.replace(/\s|[^\u0000-\u007F]/gu, encodeURIComponent);
|
|
}
|
|
/**
|
|
* Builds the dev-mode CSS styles URL for route-scoped CSS collection.
|
|
* Used by HeadContent components in all framework implementations to construct
|
|
* the URL for the `/@tanstack-start/styles.css` endpoint.
|
|
*
|
|
* @param basepath - The router's basepath (may or may not have leading slash)
|
|
* @param routeIds - Array of matched route IDs to include in the CSS collection
|
|
* @returns The full URL path for the dev styles CSS endpoint
|
|
*/
|
|
function buildDevStylesUrl(basepath, routeIds) {
|
|
const trimmedBasepath = basepath.replace(/^\/+|\/+$/g, "");
|
|
return `${trimmedBasepath === "" ? "" : `/${trimmedBasepath}`}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(","))}`;
|
|
}
|
|
function arraysEqual(a, b) {
|
|
if (a === b) return true;
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
return true;
|
|
}
|
|
//#endregion
|
|
exports.DEFAULT_PROTOCOL_ALLOWLIST = DEFAULT_PROTOCOL_ALLOWLIST;
|
|
exports.arraysEqual = arraysEqual;
|
|
exports.buildDevStylesUrl = buildDevStylesUrl;
|
|
exports.createControlledPromise = createControlledPromise;
|
|
exports.decodePath = decodePath;
|
|
exports.deepEqual = deepEqual;
|
|
exports.encodePathLikeUrl = encodePathLikeUrl;
|
|
exports.escapeHtml = escapeHtml;
|
|
exports.findLast = findLast;
|
|
exports.functionalUpdate = functionalUpdate;
|
|
exports.hasKeys = hasKeys;
|
|
exports.isDangerousProtocol = isDangerousProtocol;
|
|
exports.isModuleNotFoundError = isModuleNotFoundError;
|
|
exports.isPlainArray = isPlainArray;
|
|
exports.isPlainObject = isPlainObject;
|
|
exports.isPromise = isPromise;
|
|
exports.last = last;
|
|
exports.nullReplaceEqualDeep = nullReplaceEqualDeep;
|
|
exports.replaceEqualDeep = replaceEqualDeep;
|
|
|
|
//# sourceMappingURL=utils.cjs.map |