UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

316 lines (315 loc) 10.7 kB
import { isServer } from "@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 (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 export { DEFAULT_PROTOCOL_ALLOWLIST, buildDevStylesUrl, createControlledPromise, decodePath, deepEqual, encodePathLikeUrl, escapeHtml, findLast, functionalUpdate, isDangerousProtocol, isModuleNotFoundError, isPlainArray, isPlainObject, isPromise, last, nullReplaceEqualDeep, replaceEqualDeep }; //# sourceMappingURL=utils.js.map