@tanstack/router-core
Version:
Modern and scalable routing for React applications
333 lines (332 loc) • 11.2 kB
JavaScript
require("./_virtual/_rolldown/runtime.cjs");
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;
}
var hasOwn = Object.prototype.hasOwnProperty;
var isEnumerable = Object.prototype.propertyIsEnumerable;
var createNull = () => Object.create(null);
var 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.
*/
var 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;
}
}
var HTML_ESCAPE_LOOKUP = {
"&": "\\u0026",
">": "\\u003e",
"<": "\\u003c",
"\u2028": "\\u2028",
"\u2029": "\\u2029"
};
var 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(","))}`;
}
//#endregion
exports.DEFAULT_PROTOCOL_ALLOWLIST = DEFAULT_PROTOCOL_ALLOWLIST;
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.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