@tanstack/router-core 1.158.0 → 1.158.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/load-matches.cjs +38 -27
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/router.cjs +5 -5
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/ssr/createRequestHandler.cjs +1 -1
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +7 -2
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.d.cts +4 -1
- package/dist/cjs/utils.cjs +7 -5
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +4 -1
- package/dist/esm/load-matches.js +38 -27
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.js +5 -5
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/createRequestHandler.js +1 -1
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/ssr-server.d.ts +4 -1
- package/dist/esm/ssr/ssr-server.js +7 -2
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/utils.d.ts +4 -1
- package/dist/esm/utils.js +7 -5
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/load-matches.ts +64 -49
- package/src/router.ts +5 -5
- package/src/ssr/createRequestHandler.ts +1 -1
- package/src/ssr/ssr-server.ts +7 -2
- package/src/utils.ts +7 -7
|
@@ -14,7 +14,7 @@ function createRequestHandler({
|
|
|
14
14
|
router,
|
|
15
15
|
manifest: await getRouterManifest?.()
|
|
16
16
|
});
|
|
17
|
-
const url = getNormalizedURL(request.url, "http://localhost");
|
|
17
|
+
const { url } = getNormalizedURL(request.url, "http://localhost");
|
|
18
18
|
const origin = getOrigin(request);
|
|
19
19
|
const href = url.href.replace(url.origin, "");
|
|
20
20
|
const history = createMemoryHistory({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createRequestHandler.js","sources":["../../../src/ssr/createRequestHandler.ts"],"sourcesContent":["import { createMemoryHistory } from '@tanstack/history'\nimport { mergeHeaders } from './headers'\nimport {\n attachRouterServerSsrUtils,\n getNormalizedURL,\n getOrigin,\n} from './ssr-server'\nimport type { HandlerCallback } from './handlerCallback'\nimport type { AnyHeaders } from './headers'\nimport type { AnyRouter } from '../router'\nimport type { Manifest } from '../manifest'\n\nexport type RequestHandler<TRouter extends AnyRouter> = (\n cb: HandlerCallback<TRouter>,\n) => Promise<Response>\n\nexport function createRequestHandler<TRouter extends AnyRouter>({\n createRouter,\n request,\n getRouterManifest,\n}: {\n createRouter: () => TRouter\n request: Request\n getRouterManifest?: () => Manifest | Promise<Manifest>\n}): RequestHandler<TRouter> {\n return async (cb) => {\n const router = createRouter()\n // Track whether the callback will handle cleanup\n let cbWillCleanup = false\n\n try {\n attachRouterServerSsrUtils({\n router,\n manifest: await getRouterManifest?.(),\n })\n\n // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.\n const url = getNormalizedURL(request.url, 'http://localhost')\n const origin = getOrigin(request)\n const href = url.href.replace(url.origin, '')\n\n // Create a history for the router\n const history = createMemoryHistory({\n initialEntries: [href],\n })\n\n // Update the router with the history and context\n router.update({\n history,\n origin: router.options.origin ?? origin,\n })\n\n await router.load()\n\n await router.serverSsr?.dehydrate()\n\n const responseHeaders = getRequestHeaders({\n router,\n })\n\n // Mark that the callback will handle cleanup\n cbWillCleanup = true\n return cb({\n request,\n router,\n responseHeaders,\n } as any)\n } finally {\n if (!cbWillCleanup) {\n // Clean up router SSR state if the callback won't handle it\n // (e.g., if an error occurred before the callback was invoked).\n // When the callback runs, it handles cleanup (either via transformStreamWithRouter\n // for streaming, or directly in renderRouterToString for non-streaming).\n router.serverSsr?.cleanup()\n }\n }\n }\n}\n\nfunction getRequestHeaders(opts: { router: AnyRouter }): Headers {\n const matchHeaders = opts.router.state.matches.map<AnyHeaders>(\n (match) => match.headers,\n )\n\n // Handle Redirects\n const { redirect } = opts.router.state\n if (redirect) {\n matchHeaders.push(redirect.headers)\n }\n\n return mergeHeaders(\n {\n 'Content-Type': 'text/html; charset=UTF-8',\n },\n ...matchHeaders,\n )\n}\n"],"names":[],"mappings":";;;AAgBO,SAAS,qBAAgD;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AACF,GAI4B;AAC1B,SAAO,OAAO,OAAO;AACnB,UAAM,SAAS,aAAA;AAEf,QAAI,gBAAgB;AAEpB,QAAI;AACF,iCAA2B;AAAA,QACzB;AAAA,QACA,UAAU,MAAM,oBAAA;AAAA,MAAoB,CACrC;AAGD,YAAM,
|
|
1
|
+
{"version":3,"file":"createRequestHandler.js","sources":["../../../src/ssr/createRequestHandler.ts"],"sourcesContent":["import { createMemoryHistory } from '@tanstack/history'\nimport { mergeHeaders } from './headers'\nimport {\n attachRouterServerSsrUtils,\n getNormalizedURL,\n getOrigin,\n} from './ssr-server'\nimport type { HandlerCallback } from './handlerCallback'\nimport type { AnyHeaders } from './headers'\nimport type { AnyRouter } from '../router'\nimport type { Manifest } from '../manifest'\n\nexport type RequestHandler<TRouter extends AnyRouter> = (\n cb: HandlerCallback<TRouter>,\n) => Promise<Response>\n\nexport function createRequestHandler<TRouter extends AnyRouter>({\n createRouter,\n request,\n getRouterManifest,\n}: {\n createRouter: () => TRouter\n request: Request\n getRouterManifest?: () => Manifest | Promise<Manifest>\n}): RequestHandler<TRouter> {\n return async (cb) => {\n const router = createRouter()\n // Track whether the callback will handle cleanup\n let cbWillCleanup = false\n\n try {\n attachRouterServerSsrUtils({\n router,\n manifest: await getRouterManifest?.(),\n })\n\n // normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.\n const { url } = getNormalizedURL(request.url, 'http://localhost')\n const origin = getOrigin(request)\n const href = url.href.replace(url.origin, '')\n\n // Create a history for the router\n const history = createMemoryHistory({\n initialEntries: [href],\n })\n\n // Update the router with the history and context\n router.update({\n history,\n origin: router.options.origin ?? origin,\n })\n\n await router.load()\n\n await router.serverSsr?.dehydrate()\n\n const responseHeaders = getRequestHeaders({\n router,\n })\n\n // Mark that the callback will handle cleanup\n cbWillCleanup = true\n return cb({\n request,\n router,\n responseHeaders,\n } as any)\n } finally {\n if (!cbWillCleanup) {\n // Clean up router SSR state if the callback won't handle it\n // (e.g., if an error occurred before the callback was invoked).\n // When the callback runs, it handles cleanup (either via transformStreamWithRouter\n // for streaming, or directly in renderRouterToString for non-streaming).\n router.serverSsr?.cleanup()\n }\n }\n }\n}\n\nfunction getRequestHeaders(opts: { router: AnyRouter }): Headers {\n const matchHeaders = opts.router.state.matches.map<AnyHeaders>(\n (match) => match.headers,\n )\n\n // Handle Redirects\n const { redirect } = opts.router.state\n if (redirect) {\n matchHeaders.push(redirect.headers)\n }\n\n return mergeHeaders(\n {\n 'Content-Type': 'text/html; charset=UTF-8',\n },\n ...matchHeaders,\n )\n}\n"],"names":[],"mappings":";;;AAgBO,SAAS,qBAAgD;AAAA,EAC9D;AAAA,EACA;AAAA,EACA;AACF,GAI4B;AAC1B,SAAO,OAAO,OAAO;AACnB,UAAM,SAAS,aAAA;AAEf,QAAI,gBAAgB;AAEpB,QAAI;AACF,iCAA2B;AAAA,QACzB;AAAA,QACA,UAAU,MAAM,oBAAA;AAAA,MAAoB,CACrC;AAGD,YAAM,EAAE,IAAA,IAAQ,iBAAiB,QAAQ,KAAK,kBAAkB;AAChE,YAAM,SAAS,UAAU,OAAO;AAChC,YAAM,OAAO,IAAI,KAAK,QAAQ,IAAI,QAAQ,EAAE;AAG5C,YAAM,UAAU,oBAAoB;AAAA,QAClC,gBAAgB,CAAC,IAAI;AAAA,MAAA,CACtB;AAGD,aAAO,OAAO;AAAA,QACZ;AAAA,QACA,QAAQ,OAAO,QAAQ,UAAU;AAAA,MAAA,CAClC;AAED,YAAM,OAAO,KAAA;AAEb,YAAM,OAAO,WAAW,UAAA;AAExB,YAAM,kBAAkB,kBAAkB;AAAA,QACxC;AAAA,MAAA,CACD;AAGD,sBAAgB;AAChB,aAAO,GAAG;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,MAAA,CACM;AAAA,IACV,UAAA;AACE,UAAI,CAAC,eAAe;AAKlB,eAAO,WAAW,QAAA;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,MAAsC;AAC/D,QAAM,eAAe,KAAK,OAAO,MAAM,QAAQ;AAAA,IAC7C,CAAC,UAAU,MAAM;AAAA,EAAA;AAInB,QAAM,EAAE,SAAA,IAAa,KAAK,OAAO;AACjC,MAAI,UAAU;AACZ,iBAAa,KAAK,SAAS,OAAO;AAAA,EACpC;AAEA,SAAO;AAAA,IACL;AAAA,MACE,gBAAgB;AAAA,IAAA;AAAA,IAElB,GAAG;AAAA,EAAA;AAEP;"}
|
|
@@ -36,4 +36,7 @@ export declare function attachRouterServerSsrUtils({ router, manifest, }: {
|
|
|
36
36
|
* use the router's `origin` option to explicitly configure a trusted origin.
|
|
37
37
|
*/
|
|
38
38
|
export declare function getOrigin(request: Request): string;
|
|
39
|
-
export declare function getNormalizedURL(url: string | URL, base?: string | URL):
|
|
39
|
+
export declare function getNormalizedURL(url: string | URL, base?: string | URL): {
|
|
40
|
+
url: URL;
|
|
41
|
+
handledProtocolRelativeURL: boolean;
|
|
42
|
+
};
|
|
@@ -289,10 +289,15 @@ function getOrigin(request) {
|
|
|
289
289
|
function getNormalizedURL(url, base) {
|
|
290
290
|
if (typeof url === "string") url = url.replace("\\", "%5C");
|
|
291
291
|
const rawUrl = new URL(url, base);
|
|
292
|
-
const decodedPathname = decodePath(
|
|
292
|
+
const { path: decodedPathname, handledProtocolRelativeURL } = decodePath(
|
|
293
|
+
rawUrl.pathname
|
|
294
|
+
);
|
|
293
295
|
const searchParams = new URLSearchParams(rawUrl.search);
|
|
294
296
|
const normalizedHref = decodedPathname + (searchParams.size > 0 ? "?" : "") + searchParams.toString() + rawUrl.hash;
|
|
295
|
-
return
|
|
297
|
+
return {
|
|
298
|
+
url: new URL(normalizedHref, rawUrl.origin),
|
|
299
|
+
handledProtocolRelativeURL
|
|
300
|
+
};
|
|
296
301
|
}
|
|
297
302
|
export {
|
|
298
303
|
attachRouterServerSsrUtils,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport { createLRUCache } from '../lru-cache'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { LRUCache } from '../lru-cache'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nconst TSR_PREFIX = GLOBAL_TSR + '.router='\nconst P_PREFIX = GLOBAL_TSR + '.p(()=>'\nconst P_SUFFIX = ')'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Optimization: if only one script, avoid join\n if (bufferedScripts.length === 1) {\n return bufferedScripts[0] + ';document.currentScript.remove()'\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nconst isProd = process.env.NODE_ENV === 'production'\n\ntype FilteredRoutes = Manifest['routes']\n\ntype ManifestLRU = LRUCache<string, FilteredRoutes>\n\nconst MANIFEST_CACHE_SIZE = 100\nconst manifestCaches = new WeakMap<Manifest, ManifestLRU>()\n\nfunction getManifestCache(manifest: Manifest): ManifestLRU {\n const cache = manifestCaches.get(manifest)\n if (cache) return cache\n const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)\n manifestCaches.set(manifest, newCache)\n return newCache\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer = ''\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer += html\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n // Prod-only caching; in dev manifests may be replaced/updated (HMR)\n const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)\n const manifestCacheKey = currentRouteIdsList.join('\\0')\n\n let filteredRoutes: FilteredRoutes | undefined\n\n if (isProd) {\n filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)\n }\n\n if (!filteredRoutes) {\n const currentRouteIds = new Set(currentRouteIdsList)\n const nextFilteredRoutes: FilteredRoutes = {}\n\n for (const routeId in manifest.routes) {\n const routeManifest = manifest.routes[routeId]!\n if (currentRouteIds.has(routeId)) {\n nextFilteredRoutes[routeId] = routeManifest\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n nextFilteredRoutes[routeId] = {\n assets: routeManifest.assets,\n }\n }\n }\n\n if (isProd) {\n getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)\n }\n\n filteredRoutes = nextFilteredRoutes\n }\n\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? TSR_PREFIX + data : data\n if (trackPlugins.didRun) {\n serialized = P_PREFIX + serialized + P_SUFFIX\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (!injectedHtmlBuffer) {\n return undefined\n }\n const buffered = injectedHtmlBuffer\n injectedHtmlBuffer = ''\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = ''\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"대\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const decodedPathname = decodePath(rawUrl.pathname)\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return new URL(normalizedHref, rawUrl.origin)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport { createLRUCache } from '../lru-cache'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { LRUCache } from '../lru-cache'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nconst TSR_PREFIX = GLOBAL_TSR + '.router='\nconst P_PREFIX = GLOBAL_TSR + '.p(()=>'\nconst P_SUFFIX = ')'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Optimization: if only one script, avoid join\n if (bufferedScripts.length === 1) {\n return bufferedScripts[0] + ';document.currentScript.remove()'\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nconst isProd = process.env.NODE_ENV === 'production'\n\ntype FilteredRoutes = Manifest['routes']\n\ntype ManifestLRU = LRUCache<string, FilteredRoutes>\n\nconst MANIFEST_CACHE_SIZE = 100\nconst manifestCaches = new WeakMap<Manifest, ManifestLRU>()\n\nfunction getManifestCache(manifest: Manifest): ManifestLRU {\n const cache = manifestCaches.get(manifest)\n if (cache) return cache\n const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)\n manifestCaches.set(manifest, newCache)\n return newCache\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer = ''\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer += html\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n // Prod-only caching; in dev manifests may be replaced/updated (HMR)\n const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)\n const manifestCacheKey = currentRouteIdsList.join('\\0')\n\n let filteredRoutes: FilteredRoutes | undefined\n\n if (isProd) {\n filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)\n }\n\n if (!filteredRoutes) {\n const currentRouteIds = new Set(currentRouteIdsList)\n const nextFilteredRoutes: FilteredRoutes = {}\n\n for (const routeId in manifest.routes) {\n const routeManifest = manifest.routes[routeId]!\n if (currentRouteIds.has(routeId)) {\n nextFilteredRoutes[routeId] = routeManifest\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n nextFilteredRoutes[routeId] = {\n assets: routeManifest.assets,\n }\n }\n }\n\n if (isProd) {\n getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)\n }\n\n filteredRoutes = nextFilteredRoutes\n }\n\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? TSR_PREFIX + data : data\n if (trackPlugins.didRun) {\n serialized = P_PREFIX + serialized + P_SUFFIX\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (!injectedHtmlBuffer) {\n return undefined\n }\n const buffered = injectedHtmlBuffer\n injectedHtmlBuffer = ''\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = ''\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"대\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const { path: decodedPathname, handledProtocolRelativeURL } = decodePath(\n rawUrl.pathname,\n )\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return {\n url: new URL(normalizedHref, rawUrl.origin),\n handledProtocolRelativeURL,\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -139,7 +139,10 @@ export declare function isDangerousProtocol(url: string): boolean;
|
|
|
139
139
|
* content is embedded in inline scripts.
|
|
140
140
|
*/
|
|
141
141
|
export declare function escapeHtml(str: string): string;
|
|
142
|
-
export declare function decodePath(path: string
|
|
142
|
+
export declare function decodePath(path: string): {
|
|
143
|
+
path: string;
|
|
144
|
+
handledProtocolRelativeURL: boolean;
|
|
145
|
+
};
|
|
143
146
|
/**
|
|
144
147
|
* Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing.
|
|
145
148
|
*
|
package/dist/esm/utils.js
CHANGED
|
@@ -206,12 +206,12 @@ const HTML_ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
|
|
206
206
|
function escapeHtml(str) {
|
|
207
207
|
return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]);
|
|
208
208
|
}
|
|
209
|
-
function decodePath(path
|
|
210
|
-
if (!path) return path;
|
|
209
|
+
function decodePath(path) {
|
|
210
|
+
if (!path) return { path, handledProtocolRelativeURL: false };
|
|
211
211
|
if (!/[%\\\x00-\x1f\x7f]/.test(path) && !path.startsWith("//")) {
|
|
212
|
-
return path;
|
|
212
|
+
return { path, handledProtocolRelativeURL: false };
|
|
213
213
|
}
|
|
214
|
-
const re =
|
|
214
|
+
const re = /%25|%5C/gi;
|
|
215
215
|
let cursor = 0;
|
|
216
216
|
let result = "";
|
|
217
217
|
let match;
|
|
@@ -220,10 +220,12 @@ function decodePath(path, decodeIgnore) {
|
|
|
220
220
|
cursor = re.lastIndex;
|
|
221
221
|
}
|
|
222
222
|
result = result + decodeSegment(cursor ? path.slice(cursor) : path);
|
|
223
|
+
let handledProtocolRelativeURL = false;
|
|
223
224
|
if (result.startsWith("//")) {
|
|
225
|
+
handledProtocolRelativeURL = true;
|
|
224
226
|
result = "/" + result.replace(/^\/+/, "");
|
|
225
227
|
}
|
|
226
|
-
return result;
|
|
228
|
+
return { path: result, handledProtocolRelativeURL };
|
|
227
229
|
}
|
|
228
230
|
function encodePathLikeUrl(path) {
|
|
229
231
|
if (!/\s|[^\u0000-\u007F]/.test(path)) return path;
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport type { RouteIds } from './routeInfo'\nimport type { AnyRouter } from './router'\n\nexport type Awaitable<T> = T | Promise<T>\nexport type NoInfer<T> = [T][T extends any ? 0 : never]\nexport type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue\n ? TYesResult\n : TNoResult\n\nexport type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<\n TValue,\n TKey\n> &\n Required<Pick<TValue, TKey>>\n\nexport type PickRequired<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\nexport type PickOptional<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]: T[K]\n}\n\n// from https://stackoverflow.com/a/76458160\nexport type WithoutEmpty<T> = T extends any ? ({} extends T ? never : T) : never\n\nexport type Expand<T> = T extends object\n ? T extends infer O\n ? O extends Function\n ? O\n : { [K in keyof O]: O[K] }\n : never\n : T\n\nexport type DeepPartial<T> = T extends object\n ? {\n [P in keyof T]?: DeepPartial<T[P]>\n }\n : T\n\nexport type MakeDifferenceOptional<TLeft, TRight> = keyof TLeft &\n keyof TRight extends never\n ? TRight\n : Omit<TRight, keyof TLeft & keyof TRight> & {\n [K in keyof TLeft & keyof TRight]?: TRight[K]\n }\n\n// from https://stackoverflow.com/a/53955431\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type IsUnion<T, U extends T = T> = (\n T extends any ? (U extends T ? false : true) : never\n) extends false\n ? false\n : true\n\nexport type IsNonEmptyObject<T> = T extends object\n ? keyof T extends never\n ? false\n : true\n : false\n\nexport type Assign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : keyof TLeft & keyof TRight extends never\n ? TLeft & TRight\n : Omit<TLeft, keyof TRight> & TRight\n : never\n : never\n\nexport type IntersectAssign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : TRight & TLeft\n : never\n : never\n\nexport type Timeout = ReturnType<typeof setTimeout>\n\nexport type Updater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev?: TPrevious) => TResult)\n\nexport type NonNullableUpdater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev: TPrevious) => TResult)\n\nexport type ExtractObjects<TUnion> = TUnion extends MergeAllPrimitive\n ? never\n : TUnion\n\nexport type PartialMergeAllObject<TUnion> =\n ExtractObjects<TUnion> extends infer TObj\n ? [TObj] extends [never]\n ? never\n : {\n [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any\n ? TKey extends keyof TObj\n ? TObj[TKey]\n : never\n : never\n }\n : never\n\nexport type MergeAllPrimitive =\n | ReadonlyArray<any>\n | number\n | string\n | bigint\n | boolean\n | symbol\n | undefined\n | null\n\nexport type ExtractPrimitives<TUnion> = TUnion extends MergeAllPrimitive\n ? TUnion\n : TUnion extends object\n ? never\n : TUnion\n\nexport type PartialMergeAll<TUnion> =\n | ExtractPrimitives<TUnion>\n | PartialMergeAllObject<TUnion>\n\nexport type Constrain<T, TConstraint, TDefault = TConstraint> =\n | (T extends TConstraint ? T : never)\n | TDefault\n\nexport type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =\n | (T & TConstraint)\n | TDefault\n\n/**\n * To be added to router types\n */\nexport type UnionToIntersection<T> = (\n T extends any ? (arg: T) => any : never\n) extends (arg: infer T) => any\n ? T\n : never\n\n/**\n * Merges everything in a union into one object.\n * This mapped type is homomorphic which means it preserves stuff! :)\n */\nexport type MergeAllObjects<\n TUnion,\n TIntersected = UnionToIntersection<ExtractObjects<TUnion>>,\n> = [keyof TIntersected] extends [never]\n ? never\n : {\n [TKey in keyof TIntersected]: TUnion extends any\n ? TUnion[TKey & keyof TUnion]\n : never\n }\n\nexport type MergeAll<TUnion> =\n | MergeAllObjects<TUnion>\n | ExtractPrimitives<TUnion>\n\nexport type ValidateJSON<T> = ((...args: Array<any>) => any) extends T\n ? unknown extends T\n ? never\n : 'Function is not serializable'\n : { [K in keyof T]: ValidateJSON<T[K]> }\n\nexport type LooseReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\nexport type LooseAsyncReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn extends Promise<infer TReturn>\n ? TReturn\n : TReturn\n : never\n\n/**\n * Return the last element of an array.\n * Intended for non-empty arrays used within router internals.\n */\nexport function last<T>(arr: ReadonlyArray<T>) {\n return arr[arr.length - 1]\n}\n\nfunction isFunction(d: any): d is Function {\n return typeof d === 'function'\n}\n\n/**\n * Apply a value-or-updater to a previous value.\n * Accepts either a literal value or a function of the previous value.\n */\nexport function functionalUpdate<TPrevious, TResult = TPrevious>(\n updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,\n previous: TPrevious,\n): TResult {\n if (isFunction(updater)) {\n return updater(previous)\n }\n\n return updater\n}\n\nconst hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\n/**\n * This function returns `prev` if `_next` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between immutable JSON values for example.\n * Do not use this with signals\n */\nexport function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {\n if (isServer) {\n return _next\n }\n if (prev === _next) {\n return prev\n }\n\n if (_depth > 500) return _next\n\n const next = _next as any\n\n const array = isPlainArray(prev) && isPlainArray(next)\n\n if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next\n\n const prevItems = array ? prev : getEnumerableOwnKeys(prev)\n if (!prevItems) return next\n const nextItems = array ? next : getEnumerableOwnKeys(next)\n if (!nextItems) return next\n const prevSize = prevItems.length\n const nextSize = nextItems.length\n const copy: any = array ? new Array(nextSize) : {}\n\n let equalItems = 0\n\n for (let i = 0; i < nextSize; i++) {\n const key = array ? i : (nextItems[i] as any)\n const p = prev[key]\n const n = next[key]\n\n if (p === n) {\n copy[key] = p\n if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++\n continue\n }\n\n if (\n p === null ||\n n === null ||\n typeof p !== 'object' ||\n typeof n !== 'object'\n ) {\n copy[key] = n\n continue\n }\n\n const v = replaceEqualDeep(p, n, _depth + 1)\n copy[key] = v\n if (v === p) equalItems++\n }\n\n return prevSize === nextSize && equalItems === prevSize ? prev : copy\n}\n\n/**\n * Equivalent to `Reflect.ownKeys`, but ensures that objects are \"clone-friendly\":\n * will return false if object has any non-enumerable properties.\n *\n * Optimized for the common case where objects have no symbol properties.\n */\nfunction getEnumerableOwnKeys(o: object) {\n const names = Object.getOwnPropertyNames(o)\n\n // Fast path: check all string property names are enumerable\n for (const name of names) {\n if (!isEnumerable.call(o, name)) return false\n }\n\n // Only check symbols if the object has any (most plain objects don't)\n const symbols = Object.getOwnPropertySymbols(o)\n\n // Fast path: no symbols, return names directly (avoids array allocation/concat)\n if (symbols.length === 0) return names\n\n // Slow path: has symbols, need to check and merge\n const keys: Array<string | symbol> = names\n for (const symbol of symbols) {\n if (!isEnumerable.call(o, symbol)) return false\n keys.push(symbol)\n }\n return keys\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\nexport function isPlainObject(o: any) {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n if (typeof ctor === 'undefined') {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any) {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\n/**\n * Check if a value is a \"plain\" array (no extra enumerable keys).\n */\nexport function isPlainArray(value: unknown): value is Array<unknown> {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n/**\n * Perform a deep equality check with options for partial comparison and\n * ignoring `undefined` values. Optimized for router state comparisons.\n */\nexport function deepEqual(\n a: any,\n b: any,\n opts?: { partial?: boolean; ignoreUndefined?: boolean },\n): boolean {\n if (a === b) {\n return true\n }\n\n if (typeof a !== typeof b) {\n return false\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0, l = a.length; i < l; i++) {\n if (!deepEqual(a[i], b[i], opts)) return false\n }\n return true\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const ignoreUndefined = opts?.ignoreUndefined ?? true\n\n if (opts?.partial) {\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n if (!deepEqual(a[k], b[k], opts)) return false\n }\n }\n return true\n }\n\n let aCount = 0\n if (!ignoreUndefined) {\n aCount = Object.keys(a).length\n } else {\n for (const k in a) {\n if (a[k] !== undefined) aCount++\n }\n }\n\n let bCount = 0\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n bCount++\n if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false\n }\n }\n\n return aCount === bCount\n }\n\n return false\n}\n\nexport type StringLiteral<T> = T extends string\n ? string extends T\n ? string\n : T\n : never\n\nexport type ThrowOrOptional<T, TThrow extends boolean> = TThrow extends true\n ? T\n : T | undefined\n\nexport type StrictOrFrom<\n TRouter extends AnyRouter,\n TFrom,\n TStrict extends boolean = true,\n> = TStrict extends false\n ? {\n from?: never\n strict: TStrict\n }\n : {\n from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>\n strict?: TStrict\n }\n\nexport type ThrowConstraint<\n TStrict extends boolean,\n TThrow extends boolean,\n> = TStrict extends false ? (TThrow extends true ? never : TThrow) : TThrow\n\nexport type ControlledPromise<T> = Promise<T> & {\n resolve: (value: T) => void\n reject: (value: any) => void\n status: 'pending' | 'resolved' | 'rejected'\n value?: T\n}\n\n/**\n * Create a promise with exposed resolve/reject and status fields.\n * Useful for coordinating async router lifecycle operations.\n */\nexport function createControlledPromise<T>(onResolve?: (value: T) => void) {\n let resolveLoadPromise!: (value: T) => void\n let rejectLoadPromise!: (value: any) => void\n\n const controlledPromise = new Promise<T>((resolve, reject) => {\n resolveLoadPromise = resolve\n rejectLoadPromise = reject\n }) as ControlledPromise<T>\n\n controlledPromise.status = 'pending'\n\n controlledPromise.resolve = (value: T) => {\n controlledPromise.status = 'resolved'\n controlledPromise.value = value\n resolveLoadPromise(value)\n onResolve?.(value)\n }\n\n controlledPromise.reject = (e) => {\n controlledPromise.status = 'rejected'\n rejectLoadPromise(e)\n }\n\n return controlledPromise\n}\n\n/**\n * Heuristically detect dynamic import \"module not found\" errors\n * across major browsers for lazy route component handling.\n */\nexport function isModuleNotFoundError(error: any): boolean {\n // chrome: \"Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // firefox: \"error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // safari: \"Importing a module script failed.\"\n if (typeof error?.message !== 'string') return false\n return (\n error.message.startsWith('Failed to fetch dynamically imported module') ||\n error.message.startsWith('error loading dynamically imported module') ||\n error.message.startsWith('Importing a module script failed')\n )\n}\n\nexport function isPromise<T>(\n value: Promise<Awaited<T>> | T,\n): value is Promise<Awaited<T>> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n typeof (value as Promise<T>).then === 'function',\n )\n}\n\nexport function findLast<T>(\n array: ReadonlyArray<T>,\n predicate: (item: T) => boolean,\n): T | undefined {\n for (let i = array.length - 1; i >= 0; i--) {\n const item = array[i]!\n if (predicate(item)) return item\n }\n return undefined\n}\n\n/**\n * Remove control characters that can cause open redirect vulnerabilities.\n * Characters like \\r (CR) and \\n (LF) can trick URL parsers into interpreting\n * paths like \"/\\r/evil.com\" as \"http://evil.com\".\n */\nfunction sanitizePathSegment(segment: string): string {\n // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)\n // These include CR (\\r = 0x0D), LF (\\n = 0x0A), and other potentially dangerous characters\n // eslint-disable-next-line no-control-regex\n return segment.replace(/[\\x00-\\x1f\\x7f]/g, '')\n}\n\nfunction decodeSegment(segment: string): string {\n let decoded: string\n try {\n decoded = decodeURI(segment)\n } catch {\n // if the decoding fails, try to decode the various parts leaving the malformed tags in place\n decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {\n try {\n return decodeURI(match)\n } catch {\n return match\n }\n })\n }\n return sanitizePathSegment(decoded)\n}\n\n/**\n * List of URL protocols that are safe for navigation.\n * Only these protocols are allowed in redirects and navigation.\n */\nexport const SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:']\n\n/**\n * Check if a URL string uses a protocol that is not in the safe list.\n * Returns true for dangerous protocols like javascript:, data:, vbscript:, etc.\n *\n * The URL constructor correctly normalizes:\n * - Mixed case (JavaScript: → javascript:)\n * - Whitespace/control characters (java\\nscript: → javascript:)\n * - Leading whitespace\n *\n * For relative URLs (no protocol), returns false (safe).\n *\n * @param url - The URL string to check\n * @returns true if the URL uses a dangerous (non-whitelisted) protocol\n */\nexport function isDangerousProtocol(url: string): boolean {\n if (!url) return false\n\n try {\n // Use the URL constructor - it correctly normalizes protocols\n // per WHATWG URL spec, handling all bypass attempts automatically\n const parsed = new URL(url)\n return !SAFE_URL_PROTOCOLS.includes(parsed.protocol)\n } catch {\n // URL constructor throws for relative URLs (no protocol)\n // These are safe - they can't execute scripts\n return false\n }\n}\n\n// This utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\nconst HTML_ESCAPE_LOOKUP: { [match: string]: string } = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\nconst HTML_ESCAPE_REGEX = /[&><\\u2028\\u2029]/g\n\n/**\n * Escape HTML special characters in a string to prevent XSS attacks\n * when embedding strings in script tags during SSR.\n *\n * This is essential for preventing XSS vulnerabilities when user-controlled\n * content is embedded in inline scripts.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)\n}\n\nexport function decodePath(path: string, decodeIgnore?: Array<string>): string {\n if (!path) return path\n\n // Fast path: most paths are already decoded and safe.\n // Only fall back to the slower scan/regex path when we see a '%' (encoded),\n // a backslash (explicitly handled), a control character, or a protocol-relative\n // prefix which needs collapsing.\n // eslint-disable-next-line no-control-regex\n if (!/[%\\\\\\x00-\\x1f\\x7f]/.test(path) && !path.startsWith('//')) {\n return path\n }\n\n const re = decodeIgnore\n ? new RegExp(`${decodeIgnore.join('|')}`, 'gi')\n : /%25|%5C/gi\n let cursor = 0\n let result = ''\n let match\n while (null !== (match = re.exec(path))) {\n result += decodeSegment(path.slice(cursor, match.index)) + match[0]\n cursor = re.lastIndex\n }\n result = result + decodeSegment(cursor ? path.slice(cursor) : path)\n\n // Prevent open redirect via protocol-relative URLs (e.g. \"//evil.com\")\n // After sanitizing control characters, paths like \"/\\r/evil.com\" become \"//evil.com\"\n // Collapse leading double slashes to a single slash\n if (result.startsWith('//')) {\n result = '/' + result.replace(/^\\/+/, '')\n }\n\n return result\n}\n\n/**\n * Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing.\n *\n * This function encodes:\n * - Whitespace characters (spaces → %20, tabs → %09, etc.)\n * - Non-ASCII/Unicode characters (emojis, accented characters, etc.)\n *\n * It preserves:\n * - Already percent-encoded sequences (won't double-encode %2F, %25, etc.)\n * - ASCII special characters valid in URL paths (@, $, &, +, etc.)\n * - Forward slashes as path separators\n *\n * Used to generate proper href values for SSR without constructing URL objects.\n *\n * @example\n * encodePathLikeUrl('/path/file name.pdf') // '/path/file%20name.pdf'\n * encodePathLikeUrl('/path/日本語') // '/path/%E6%97%A5%E6%9C%AC%E8%AA%9E'\n * encodePathLikeUrl('/path/already%20encoded') // '/path/already%20encoded' (preserved)\n */\nexport function encodePathLikeUrl(path: string): string {\n // Encode whitespace and non-ASCII characters that browsers encode in URLs\n\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n if (!/\\s|[^\\u0000-\\u007F]/.test(path)) return path\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n return path.replace(/\\s|[^\\u0000-\\u007F]/gu, encodeURIComponent)\n}\n\n/**\n * Builds the dev-mode CSS styles URL for route-scoped CSS collection.\n * Used by HeadContent components in all framework implementations to construct\n * the URL for the `/@tanstack-start/styles.css` endpoint.\n *\n * @param basepath - The router's basepath (may or may not have leading slash)\n * @param routeIds - Array of matched route IDs to include in the CSS collection\n * @returns The full URL path for the dev styles CSS endpoint\n */\nexport function buildDevStylesUrl(\n basepath: string,\n routeIds: Array<string>,\n): string {\n // Trim all leading and trailing slashes from basepath\n const trimmedBasepath = basepath.replace(/^\\/+|\\/+$/g, '')\n // Build normalized basepath: empty string for root, or '/path' for non-root\n const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}`\n return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport type { RouteIds } from './routeInfo'\nimport type { AnyRouter } from './router'\n\nexport type Awaitable<T> = T | Promise<T>\nexport type NoInfer<T> = [T][T extends any ? 0 : never]\nexport type IsAny<TValue, TYesResult, TNoResult = TValue> = 1 extends 0 & TValue\n ? TYesResult\n : TNoResult\n\nexport type PickAsRequired<TValue, TKey extends keyof TValue> = Omit<\n TValue,\n TKey\n> &\n Required<Pick<TValue, TKey>>\n\nexport type PickRequired<T> = {\n [K in keyof T as undefined extends T[K] ? never : K]: T[K]\n}\n\nexport type PickOptional<T> = {\n [K in keyof T as undefined extends T[K] ? K : never]: T[K]\n}\n\n// from https://stackoverflow.com/a/76458160\nexport type WithoutEmpty<T> = T extends any ? ({} extends T ? never : T) : never\n\nexport type Expand<T> = T extends object\n ? T extends infer O\n ? O extends Function\n ? O\n : { [K in keyof O]: O[K] }\n : never\n : T\n\nexport type DeepPartial<T> = T extends object\n ? {\n [P in keyof T]?: DeepPartial<T[P]>\n }\n : T\n\nexport type MakeDifferenceOptional<TLeft, TRight> = keyof TLeft &\n keyof TRight extends never\n ? TRight\n : Omit<TRight, keyof TLeft & keyof TRight> & {\n [K in keyof TLeft & keyof TRight]?: TRight[K]\n }\n\n// from https://stackoverflow.com/a/53955431\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport type IsUnion<T, U extends T = T> = (\n T extends any ? (U extends T ? false : true) : never\n) extends false\n ? false\n : true\n\nexport type IsNonEmptyObject<T> = T extends object\n ? keyof T extends never\n ? false\n : true\n : false\n\nexport type Assign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : keyof TLeft & keyof TRight extends never\n ? TLeft & TRight\n : Omit<TLeft, keyof TRight> & TRight\n : never\n : never\n\nexport type IntersectAssign<TLeft, TRight> = TLeft extends any\n ? TRight extends any\n ? IsNonEmptyObject<TLeft> extends false\n ? TRight\n : IsNonEmptyObject<TRight> extends false\n ? TLeft\n : TRight & TLeft\n : never\n : never\n\nexport type Timeout = ReturnType<typeof setTimeout>\n\nexport type Updater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev?: TPrevious) => TResult)\n\nexport type NonNullableUpdater<TPrevious, TResult = TPrevious> =\n | TResult\n | ((prev: TPrevious) => TResult)\n\nexport type ExtractObjects<TUnion> = TUnion extends MergeAllPrimitive\n ? never\n : TUnion\n\nexport type PartialMergeAllObject<TUnion> =\n ExtractObjects<TUnion> extends infer TObj\n ? [TObj] extends [never]\n ? never\n : {\n [TKey in TObj extends any ? keyof TObj : never]?: TObj extends any\n ? TKey extends keyof TObj\n ? TObj[TKey]\n : never\n : never\n }\n : never\n\nexport type MergeAllPrimitive =\n | ReadonlyArray<any>\n | number\n | string\n | bigint\n | boolean\n | symbol\n | undefined\n | null\n\nexport type ExtractPrimitives<TUnion> = TUnion extends MergeAllPrimitive\n ? TUnion\n : TUnion extends object\n ? never\n : TUnion\n\nexport type PartialMergeAll<TUnion> =\n | ExtractPrimitives<TUnion>\n | PartialMergeAllObject<TUnion>\n\nexport type Constrain<T, TConstraint, TDefault = TConstraint> =\n | (T extends TConstraint ? T : never)\n | TDefault\n\nexport type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =\n | (T & TConstraint)\n | TDefault\n\n/**\n * To be added to router types\n */\nexport type UnionToIntersection<T> = (\n T extends any ? (arg: T) => any : never\n) extends (arg: infer T) => any\n ? T\n : never\n\n/**\n * Merges everything in a union into one object.\n * This mapped type is homomorphic which means it preserves stuff! :)\n */\nexport type MergeAllObjects<\n TUnion,\n TIntersected = UnionToIntersection<ExtractObjects<TUnion>>,\n> = [keyof TIntersected] extends [never]\n ? never\n : {\n [TKey in keyof TIntersected]: TUnion extends any\n ? TUnion[TKey & keyof TUnion]\n : never\n }\n\nexport type MergeAll<TUnion> =\n | MergeAllObjects<TUnion>\n | ExtractPrimitives<TUnion>\n\nexport type ValidateJSON<T> = ((...args: Array<any>) => any) extends T\n ? unknown extends T\n ? never\n : 'Function is not serializable'\n : { [K in keyof T]: ValidateJSON<T[K]> }\n\nexport type LooseReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn\n : never\n\nexport type LooseAsyncReturnType<T> = T extends (\n ...args: Array<any>\n) => infer TReturn\n ? TReturn extends Promise<infer TReturn>\n ? TReturn\n : TReturn\n : never\n\n/**\n * Return the last element of an array.\n * Intended for non-empty arrays used within router internals.\n */\nexport function last<T>(arr: ReadonlyArray<T>) {\n return arr[arr.length - 1]\n}\n\nfunction isFunction(d: any): d is Function {\n return typeof d === 'function'\n}\n\n/**\n * Apply a value-or-updater to a previous value.\n * Accepts either a literal value or a function of the previous value.\n */\nexport function functionalUpdate<TPrevious, TResult = TPrevious>(\n updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>,\n previous: TPrevious,\n): TResult {\n if (isFunction(updater)) {\n return updater(previous)\n }\n\n return updater\n}\n\nconst hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\n/**\n * This function returns `prev` if `_next` is deeply equal.\n * If not, it will replace any deeply equal children of `b` with those of `a`.\n * This can be used for structural sharing between immutable JSON values for example.\n * Do not use this with signals\n */\nexport function replaceEqualDeep<T>(prev: any, _next: T, _depth = 0): T {\n if (isServer) {\n return _next\n }\n if (prev === _next) {\n return prev\n }\n\n if (_depth > 500) return _next\n\n const next = _next as any\n\n const array = isPlainArray(prev) && isPlainArray(next)\n\n if (!array && !(isPlainObject(prev) && isPlainObject(next))) return next\n\n const prevItems = array ? prev : getEnumerableOwnKeys(prev)\n if (!prevItems) return next\n const nextItems = array ? next : getEnumerableOwnKeys(next)\n if (!nextItems) return next\n const prevSize = prevItems.length\n const nextSize = nextItems.length\n const copy: any = array ? new Array(nextSize) : {}\n\n let equalItems = 0\n\n for (let i = 0; i < nextSize; i++) {\n const key = array ? i : (nextItems[i] as any)\n const p = prev[key]\n const n = next[key]\n\n if (p === n) {\n copy[key] = p\n if (array ? i < prevSize : hasOwn.call(prev, key)) equalItems++\n continue\n }\n\n if (\n p === null ||\n n === null ||\n typeof p !== 'object' ||\n typeof n !== 'object'\n ) {\n copy[key] = n\n continue\n }\n\n const v = replaceEqualDeep(p, n, _depth + 1)\n copy[key] = v\n if (v === p) equalItems++\n }\n\n return prevSize === nextSize && equalItems === prevSize ? prev : copy\n}\n\n/**\n * Equivalent to `Reflect.ownKeys`, but ensures that objects are \"clone-friendly\":\n * will return false if object has any non-enumerable properties.\n *\n * Optimized for the common case where objects have no symbol properties.\n */\nfunction getEnumerableOwnKeys(o: object) {\n const names = Object.getOwnPropertyNames(o)\n\n // Fast path: check all string property names are enumerable\n for (const name of names) {\n if (!isEnumerable.call(o, name)) return false\n }\n\n // Only check symbols if the object has any (most plain objects don't)\n const symbols = Object.getOwnPropertySymbols(o)\n\n // Fast path: no symbols, return names directly (avoids array allocation/concat)\n if (symbols.length === 0) return names\n\n // Slow path: has symbols, need to check and merge\n const keys: Array<string | symbol> = names\n for (const symbol of symbols) {\n if (!isEnumerable.call(o, symbol)) return false\n keys.push(symbol)\n }\n return keys\n}\n\n// Copied from: https://github.com/jonschlinkert/is-plain-object\nexport function isPlainObject(o: any) {\n if (!hasObjectPrototype(o)) {\n return false\n }\n\n // If has modified constructor\n const ctor = o.constructor\n if (typeof ctor === 'undefined') {\n return true\n }\n\n // If has modified prototype\n const prot = ctor.prototype\n if (!hasObjectPrototype(prot)) {\n return false\n }\n\n // If constructor does not have an Object-specific method\n if (!prot.hasOwnProperty('isPrototypeOf')) {\n return false\n }\n\n // Most likely a plain Object\n return true\n}\n\nfunction hasObjectPrototype(o: any) {\n return Object.prototype.toString.call(o) === '[object Object]'\n}\n\n/**\n * Check if a value is a \"plain\" array (no extra enumerable keys).\n */\nexport function isPlainArray(value: unknown): value is Array<unknown> {\n return Array.isArray(value) && value.length === Object.keys(value).length\n}\n\n/**\n * Perform a deep equality check with options for partial comparison and\n * ignoring `undefined` values. Optimized for router state comparisons.\n */\nexport function deepEqual(\n a: any,\n b: any,\n opts?: { partial?: boolean; ignoreUndefined?: boolean },\n): boolean {\n if (a === b) {\n return true\n }\n\n if (typeof a !== typeof b) {\n return false\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0, l = a.length; i < l; i++) {\n if (!deepEqual(a[i], b[i], opts)) return false\n }\n return true\n }\n\n if (isPlainObject(a) && isPlainObject(b)) {\n const ignoreUndefined = opts?.ignoreUndefined ?? true\n\n if (opts?.partial) {\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n if (!deepEqual(a[k], b[k], opts)) return false\n }\n }\n return true\n }\n\n let aCount = 0\n if (!ignoreUndefined) {\n aCount = Object.keys(a).length\n } else {\n for (const k in a) {\n if (a[k] !== undefined) aCount++\n }\n }\n\n let bCount = 0\n for (const k in b) {\n if (!ignoreUndefined || b[k] !== undefined) {\n bCount++\n if (bCount > aCount || !deepEqual(a[k], b[k], opts)) return false\n }\n }\n\n return aCount === bCount\n }\n\n return false\n}\n\nexport type StringLiteral<T> = T extends string\n ? string extends T\n ? string\n : T\n : never\n\nexport type ThrowOrOptional<T, TThrow extends boolean> = TThrow extends true\n ? T\n : T | undefined\n\nexport type StrictOrFrom<\n TRouter extends AnyRouter,\n TFrom,\n TStrict extends boolean = true,\n> = TStrict extends false\n ? {\n from?: never\n strict: TStrict\n }\n : {\n from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>\n strict?: TStrict\n }\n\nexport type ThrowConstraint<\n TStrict extends boolean,\n TThrow extends boolean,\n> = TStrict extends false ? (TThrow extends true ? never : TThrow) : TThrow\n\nexport type ControlledPromise<T> = Promise<T> & {\n resolve: (value: T) => void\n reject: (value: any) => void\n status: 'pending' | 'resolved' | 'rejected'\n value?: T\n}\n\n/**\n * Create a promise with exposed resolve/reject and status fields.\n * Useful for coordinating async router lifecycle operations.\n */\nexport function createControlledPromise<T>(onResolve?: (value: T) => void) {\n let resolveLoadPromise!: (value: T) => void\n let rejectLoadPromise!: (value: any) => void\n\n const controlledPromise = new Promise<T>((resolve, reject) => {\n resolveLoadPromise = resolve\n rejectLoadPromise = reject\n }) as ControlledPromise<T>\n\n controlledPromise.status = 'pending'\n\n controlledPromise.resolve = (value: T) => {\n controlledPromise.status = 'resolved'\n controlledPromise.value = value\n resolveLoadPromise(value)\n onResolve?.(value)\n }\n\n controlledPromise.reject = (e) => {\n controlledPromise.status = 'rejected'\n rejectLoadPromise(e)\n }\n\n return controlledPromise\n}\n\n/**\n * Heuristically detect dynamic import \"module not found\" errors\n * across major browsers for lazy route component handling.\n */\nexport function isModuleNotFoundError(error: any): boolean {\n // chrome: \"Failed to fetch dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // firefox: \"error loading dynamically imported module: http://localhost:5173/src/routes/posts.index.tsx?tsr-split\"\n // safari: \"Importing a module script failed.\"\n if (typeof error?.message !== 'string') return false\n return (\n error.message.startsWith('Failed to fetch dynamically imported module') ||\n error.message.startsWith('error loading dynamically imported module') ||\n error.message.startsWith('Importing a module script failed')\n )\n}\n\nexport function isPromise<T>(\n value: Promise<Awaited<T>> | T,\n): value is Promise<Awaited<T>> {\n return Boolean(\n value &&\n typeof value === 'object' &&\n typeof (value as Promise<T>).then === 'function',\n )\n}\n\nexport function findLast<T>(\n array: ReadonlyArray<T>,\n predicate: (item: T) => boolean,\n): T | undefined {\n for (let i = array.length - 1; i >= 0; i--) {\n const item = array[i]!\n if (predicate(item)) return item\n }\n return undefined\n}\n\n/**\n * Remove control characters that can cause open redirect vulnerabilities.\n * Characters like \\r (CR) and \\n (LF) can trick URL parsers into interpreting\n * paths like \"/\\r/evil.com\" as \"http://evil.com\".\n */\nfunction sanitizePathSegment(segment: string): string {\n // Remove ASCII control characters (0x00-0x1F) and DEL (0x7F)\n // These include CR (\\r = 0x0D), LF (\\n = 0x0A), and other potentially dangerous characters\n // eslint-disable-next-line no-control-regex\n return segment.replace(/[\\x00-\\x1f\\x7f]/g, '')\n}\n\nfunction decodeSegment(segment: string): string {\n let decoded: string\n try {\n decoded = decodeURI(segment)\n } catch {\n // if the decoding fails, try to decode the various parts leaving the malformed tags in place\n decoded = segment.replaceAll(/%[0-9A-F]{2}/gi, (match) => {\n try {\n return decodeURI(match)\n } catch {\n return match\n }\n })\n }\n return sanitizePathSegment(decoded)\n}\n\n/**\n * List of URL protocols that are safe for navigation.\n * Only these protocols are allowed in redirects and navigation.\n */\nexport const SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:']\n\n/**\n * Check if a URL string uses a protocol that is not in the safe list.\n * Returns true for dangerous protocols like javascript:, data:, vbscript:, etc.\n *\n * The URL constructor correctly normalizes:\n * - Mixed case (JavaScript: → javascript:)\n * - Whitespace/control characters (java\\nscript: → javascript:)\n * - Leading whitespace\n *\n * For relative URLs (no protocol), returns false (safe).\n *\n * @param url - The URL string to check\n * @returns true if the URL uses a dangerous (non-whitelisted) protocol\n */\nexport function isDangerousProtocol(url: string): boolean {\n if (!url) return false\n\n try {\n // Use the URL constructor - it correctly normalizes protocols\n // per WHATWG URL spec, handling all bypass attempts automatically\n const parsed = new URL(url)\n return !SAFE_URL_PROTOCOLS.includes(parsed.protocol)\n } catch {\n // URL constructor throws for relative URLs (no protocol)\n // These are safe - they can't execute scripts\n return false\n }\n}\n\n// This utility is based on https://github.com/zertosh/htmlescape\n// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE\nconst HTML_ESCAPE_LOOKUP: { [match: string]: string } = {\n '&': '\\\\u0026',\n '>': '\\\\u003e',\n '<': '\\\\u003c',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\nconst HTML_ESCAPE_REGEX = /[&><\\u2028\\u2029]/g\n\n/**\n * Escape HTML special characters in a string to prevent XSS attacks\n * when embedding strings in script tags during SSR.\n *\n * This is essential for preventing XSS vulnerabilities when user-controlled\n * content is embedded in inline scripts.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)\n}\n\nexport function decodePath(path: string) {\n if (!path) return { path, handledProtocolRelativeURL: false }\n\n // Fast path: most paths are already decoded and safe.\n // Only fall back to the slower scan/regex path when we see a '%' (encoded),\n // a backslash (explicitly handled), a control character, or a protocol-relative\n // prefix which needs collapsing.\n // eslint-disable-next-line no-control-regex\n if (!/[%\\\\\\x00-\\x1f\\x7f]/.test(path) && !path.startsWith('//')) {\n return { path, handledProtocolRelativeURL: false }\n }\n\n const re = /%25|%5C/gi\n let cursor = 0\n let result = ''\n let match\n while (null !== (match = re.exec(path))) {\n result += decodeSegment(path.slice(cursor, match.index)) + match[0]\n cursor = re.lastIndex\n }\n result = result + decodeSegment(cursor ? path.slice(cursor) : path)\n\n // Prevent open redirect via protocol-relative URLs (e.g. \"//evil.com\")\n // After sanitizing control characters, paths like \"/\\r/evil.com\" become \"//evil.com\"\n // Collapse leading double slashes to a single slash\n let handledProtocolRelativeURL = false\n if (result.startsWith('//')) {\n handledProtocolRelativeURL = true\n result = '/' + result.replace(/^\\/+/, '')\n }\n\n return { path: result, handledProtocolRelativeURL }\n}\n\n/**\n * Encodes a path the same way `new URL()` would, but without the overhead of full URL parsing.\n *\n * This function encodes:\n * - Whitespace characters (spaces → %20, tabs → %09, etc.)\n * - Non-ASCII/Unicode characters (emojis, accented characters, etc.)\n *\n * It preserves:\n * - Already percent-encoded sequences (won't double-encode %2F, %25, etc.)\n * - ASCII special characters valid in URL paths (@, $, &, +, etc.)\n * - Forward slashes as path separators\n *\n * Used to generate proper href values for SSR without constructing URL objects.\n *\n * @example\n * encodePathLikeUrl('/path/file name.pdf') // '/path/file%20name.pdf'\n * encodePathLikeUrl('/path/日本語') // '/path/%E6%97%A5%E6%9C%AC%E8%AA%9E'\n * encodePathLikeUrl('/path/already%20encoded') // '/path/already%20encoded' (preserved)\n */\nexport function encodePathLikeUrl(path: string): string {\n // Encode whitespace and non-ASCII characters that browsers encode in URLs\n\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n if (!/\\s|[^\\u0000-\\u007F]/.test(path)) return path\n // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ASCII range check\n // eslint-disable-next-line no-control-regex\n return path.replace(/\\s|[^\\u0000-\\u007F]/gu, encodeURIComponent)\n}\n\n/**\n * Builds the dev-mode CSS styles URL for route-scoped CSS collection.\n * Used by HeadContent components in all framework implementations to construct\n * the URL for the `/@tanstack-start/styles.css` endpoint.\n *\n * @param basepath - The router's basepath (may or may not have leading slash)\n * @param routeIds - Array of matched route IDs to include in the CSS collection\n * @returns The full URL path for the dev styles CSS endpoint\n */\nexport function buildDevStylesUrl(\n basepath: string,\n routeIds: Array<string>,\n): string {\n // Trim all leading and trailing slashes from basepath\n const trimmedBasepath = basepath.replace(/^\\/+|\\/+$/g, '')\n // Build normalized basepath: empty string for root, or '/path' for non-root\n const normalizedBasepath = trimmedBasepath === '' ? '' : `/${trimmedBasepath}`\n return `${normalizedBasepath}/@tanstack-start/styles.css?routes=${encodeURIComponent(routeIds.join(','))}`\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/load-matches.ts
CHANGED
|
@@ -699,6 +699,11 @@ const runLoader = async (
|
|
|
699
699
|
let error = e
|
|
700
700
|
|
|
701
701
|
if ((error as any)?.name === 'AbortError') {
|
|
702
|
+
if (match.abortController.signal.aborted) {
|
|
703
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
704
|
+
match._nonReactive.loaderPromise = undefined
|
|
705
|
+
return
|
|
706
|
+
}
|
|
702
707
|
inner.updateMatch(matchId, (prev) => ({
|
|
703
708
|
...prev,
|
|
704
709
|
status: prev.status === 'pending' ? 'success' : prev.status,
|
|
@@ -749,6 +754,56 @@ const loadRouteMatch = async (
|
|
|
749
754
|
inner: InnerLoadContext,
|
|
750
755
|
index: number,
|
|
751
756
|
): Promise<AnyRouteMatch> => {
|
|
757
|
+
async function handleLoader(
|
|
758
|
+
preload: boolean,
|
|
759
|
+
prevMatch: AnyRouteMatch,
|
|
760
|
+
match: AnyRouteMatch,
|
|
761
|
+
route: AnyRoute,
|
|
762
|
+
) {
|
|
763
|
+
const age = Date.now() - prevMatch.updatedAt
|
|
764
|
+
|
|
765
|
+
const staleAge = preload
|
|
766
|
+
? (route.options.preloadStaleTime ??
|
|
767
|
+
inner.router.options.defaultPreloadStaleTime ??
|
|
768
|
+
30_000) // 30 seconds for preloads by default
|
|
769
|
+
: (route.options.staleTime ?? inner.router.options.defaultStaleTime ?? 0)
|
|
770
|
+
|
|
771
|
+
const shouldReloadOption = route.options.shouldReload
|
|
772
|
+
|
|
773
|
+
// Default to reloading the route all the time
|
|
774
|
+
// Allow shouldReload to get the last say,
|
|
775
|
+
// if provided.
|
|
776
|
+
const shouldReload =
|
|
777
|
+
typeof shouldReloadOption === 'function'
|
|
778
|
+
? shouldReloadOption(getLoaderContext(inner, matchId, index, route))
|
|
779
|
+
: shouldReloadOption
|
|
780
|
+
|
|
781
|
+
// If the route is successful and still fresh, just resolve
|
|
782
|
+
const { status, invalid } = match
|
|
783
|
+
loaderShouldRunAsync =
|
|
784
|
+
status === 'success' && (invalid || (shouldReload ?? age > staleAge))
|
|
785
|
+
if (preload && route.options.preload === false) {
|
|
786
|
+
// Do nothing
|
|
787
|
+
} else if (loaderShouldRunAsync && !inner.sync) {
|
|
788
|
+
loaderIsRunningAsync = true
|
|
789
|
+
;(async () => {
|
|
790
|
+
try {
|
|
791
|
+
await runLoader(inner, matchId, index, route)
|
|
792
|
+
const match = inner.router.getMatch(matchId)!
|
|
793
|
+
match._nonReactive.loaderPromise?.resolve()
|
|
794
|
+
match._nonReactive.loadPromise?.resolve()
|
|
795
|
+
match._nonReactive.loaderPromise = undefined
|
|
796
|
+
} catch (err) {
|
|
797
|
+
if (isRedirect(err)) {
|
|
798
|
+
await inner.router.navigate(err.options)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
})()
|
|
802
|
+
} else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
|
|
803
|
+
await runLoader(inner, matchId, index, route)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
752
807
|
const { id: matchId, routeId } = inner.matches[index]!
|
|
753
808
|
let loaderShouldRunAsync = false
|
|
754
809
|
let loaderIsRunningAsync = false
|
|
@@ -759,7 +814,9 @@ const loadRouteMatch = async (
|
|
|
759
814
|
return inner.router.getMatch(matchId)!
|
|
760
815
|
}
|
|
761
816
|
} else {
|
|
762
|
-
const prevMatch = inner.router.getMatch(matchId)!
|
|
817
|
+
const prevMatch = inner.router.getMatch(matchId)! // This is where all of the stale-while-revalidate magic happens
|
|
818
|
+
const preload = resolvePreload(inner, matchId)
|
|
819
|
+
|
|
763
820
|
// there is a loaderPromise, so we are in the middle of a load
|
|
764
821
|
if (prevMatch._nonReactive.loaderPromise) {
|
|
765
822
|
// do not block if we already have stale data we can show
|
|
@@ -774,32 +831,13 @@ const loadRouteMatch = async (
|
|
|
774
831
|
if (error) {
|
|
775
832
|
handleRedirectAndNotFound(inner, match, error)
|
|
776
833
|
}
|
|
777
|
-
} else {
|
|
778
|
-
// This is where all of the stale-while-revalidate magic happens
|
|
779
|
-
const age = Date.now() - prevMatch.updatedAt
|
|
780
|
-
|
|
781
|
-
const preload = resolvePreload(inner, matchId)
|
|
782
|
-
|
|
783
|
-
const staleAge = preload
|
|
784
|
-
? (route.options.preloadStaleTime ??
|
|
785
|
-
inner.router.options.defaultPreloadStaleTime ??
|
|
786
|
-
30_000) // 30 seconds for preloads by default
|
|
787
|
-
: (route.options.staleTime ??
|
|
788
|
-
inner.router.options.defaultStaleTime ??
|
|
789
|
-
0)
|
|
790
|
-
|
|
791
|
-
const shouldReloadOption = route.options.shouldReload
|
|
792
|
-
|
|
793
|
-
// Default to reloading the route all the time
|
|
794
|
-
// Allow shouldReload to get the last say,
|
|
795
|
-
// if provided.
|
|
796
|
-
const shouldReload =
|
|
797
|
-
typeof shouldReloadOption === 'function'
|
|
798
|
-
? shouldReloadOption(getLoaderContext(inner, matchId, index, route))
|
|
799
|
-
: shouldReloadOption
|
|
800
834
|
|
|
835
|
+
if (match.status === 'pending') {
|
|
836
|
+
await handleLoader(preload, prevMatch, match, route)
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
801
839
|
const nextPreload =
|
|
802
|
-
|
|
840
|
+
preload && !inner.router.state.matches.some((d) => d.id === matchId)
|
|
803
841
|
const match = inner.router.getMatch(matchId)!
|
|
804
842
|
match._nonReactive.loaderPromise = createControlledPromise<void>()
|
|
805
843
|
if (nextPreload !== match.preload) {
|
|
@@ -809,30 +847,7 @@ const loadRouteMatch = async (
|
|
|
809
847
|
}))
|
|
810
848
|
}
|
|
811
849
|
|
|
812
|
-
|
|
813
|
-
const { status, invalid } = match
|
|
814
|
-
loaderShouldRunAsync =
|
|
815
|
-
status === 'success' && (invalid || (shouldReload ?? age > staleAge))
|
|
816
|
-
if (preload && route.options.preload === false) {
|
|
817
|
-
// Do nothing
|
|
818
|
-
} else if (loaderShouldRunAsync && !inner.sync) {
|
|
819
|
-
loaderIsRunningAsync = true
|
|
820
|
-
;(async () => {
|
|
821
|
-
try {
|
|
822
|
-
await runLoader(inner, matchId, index, route)
|
|
823
|
-
const match = inner.router.getMatch(matchId)!
|
|
824
|
-
match._nonReactive.loaderPromise?.resolve()
|
|
825
|
-
match._nonReactive.loadPromise?.resolve()
|
|
826
|
-
match._nonReactive.loaderPromise = undefined
|
|
827
|
-
} catch (err) {
|
|
828
|
-
if (isRedirect(err)) {
|
|
829
|
-
await inner.router.navigate(err.options)
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
})()
|
|
833
|
-
} else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
|
|
834
|
-
await runLoader(inner, matchId, index, route)
|
|
835
|
-
}
|
|
850
|
+
await handleLoader(preload, prevMatch, match, route)
|
|
836
851
|
}
|
|
837
852
|
}
|
|
838
853
|
const match = inner.router.getMatch(matchId)!
|
package/src/router.ts
CHANGED
|
@@ -1268,14 +1268,14 @@ export class RouterCore<
|
|
|
1268
1268
|
return {
|
|
1269
1269
|
href: pathname + searchStr + hash,
|
|
1270
1270
|
publicHref: href,
|
|
1271
|
-
pathname: decodePath(pathname),
|
|
1271
|
+
pathname: decodePath(pathname).path,
|
|
1272
1272
|
external: false,
|
|
1273
1273
|
searchStr,
|
|
1274
1274
|
search: replaceEqualDeep(
|
|
1275
1275
|
previousLocation?.search,
|
|
1276
1276
|
parsedSearch,
|
|
1277
1277
|
) as any,
|
|
1278
|
-
hash: decodePath(hash.slice(1)),
|
|
1278
|
+
hash: decodePath(hash.slice(1)).path,
|
|
1279
1279
|
state: replaceEqualDeep(previousLocation?.state, state),
|
|
1280
1280
|
}
|
|
1281
1281
|
}
|
|
@@ -1297,11 +1297,11 @@ export class RouterCore<
|
|
|
1297
1297
|
return {
|
|
1298
1298
|
href: fullPath,
|
|
1299
1299
|
publicHref: href,
|
|
1300
|
-
pathname: decodePath(url.pathname),
|
|
1300
|
+
pathname: decodePath(url.pathname).path,
|
|
1301
1301
|
external: !!this.rewrite && url.origin !== this.origin,
|
|
1302
1302
|
searchStr,
|
|
1303
1303
|
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
|
|
1304
|
-
hash: decodePath(url.hash.slice(1)),
|
|
1304
|
+
hash: decodePath(url.hash.slice(1)).path,
|
|
1305
1305
|
state: replaceEqualDeep(previousLocation?.state, state),
|
|
1306
1306
|
}
|
|
1307
1307
|
}
|
|
@@ -1879,7 +1879,7 @@ export class RouterCore<
|
|
|
1879
1879
|
decoder: this.pathParamsDecoder,
|
|
1880
1880
|
server: this.isServer,
|
|
1881
1881
|
}).interpolatedPath,
|
|
1882
|
-
)
|
|
1882
|
+
).path
|
|
1883
1883
|
|
|
1884
1884
|
// Resolve the next search
|
|
1885
1885
|
let nextSearch = fromSearch
|
|
@@ -35,7 +35,7 @@ export function createRequestHandler<TRouter extends AnyRouter>({
|
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
// normalizing and sanitizing the pathname here for server, so we always deal with the same format during SSR.
|
|
38
|
-
const url = getNormalizedURL(request.url, 'http://localhost')
|
|
38
|
+
const { url } = getNormalizedURL(request.url, 'http://localhost')
|
|
39
39
|
const origin = getOrigin(request)
|
|
40
40
|
const href = url.href.replace(url.origin, '')
|
|
41
41
|
|
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -398,7 +398,9 @@ export function getNormalizedURL(url: string | URL, base?: string | URL) {
|
|
|
398
398
|
if (typeof url === 'string') url = url.replace('\\', '%5C')
|
|
399
399
|
|
|
400
400
|
const rawUrl = new URL(url, base)
|
|
401
|
-
const decodedPathname = decodePath(
|
|
401
|
+
const { path: decodedPathname, handledProtocolRelativeURL } = decodePath(
|
|
402
|
+
rawUrl.pathname,
|
|
403
|
+
)
|
|
402
404
|
const searchParams = new URLSearchParams(rawUrl.search)
|
|
403
405
|
const normalizedHref =
|
|
404
406
|
decodedPathname +
|
|
@@ -406,5 +408,8 @@ export function getNormalizedURL(url: string | URL, base?: string | URL) {
|
|
|
406
408
|
searchParams.toString() +
|
|
407
409
|
rawUrl.hash
|
|
408
410
|
|
|
409
|
-
return
|
|
411
|
+
return {
|
|
412
|
+
url: new URL(normalizedHref, rawUrl.origin),
|
|
413
|
+
handledProtocolRelativeURL,
|
|
414
|
+
}
|
|
410
415
|
}
|
package/src/utils.ts
CHANGED
|
@@ -593,8 +593,8 @@ export function escapeHtml(str: string): string {
|
|
|
593
593
|
return str.replace(HTML_ESCAPE_REGEX, (match) => HTML_ESCAPE_LOOKUP[match]!)
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
export function decodePath(path: string
|
|
597
|
-
if (!path) return path
|
|
596
|
+
export function decodePath(path: string) {
|
|
597
|
+
if (!path) return { path, handledProtocolRelativeURL: false }
|
|
598
598
|
|
|
599
599
|
// Fast path: most paths are already decoded and safe.
|
|
600
600
|
// Only fall back to the slower scan/regex path when we see a '%' (encoded),
|
|
@@ -602,12 +602,10 @@ export function decodePath(path: string, decodeIgnore?: Array<string>): string {
|
|
|
602
602
|
// prefix which needs collapsing.
|
|
603
603
|
// eslint-disable-next-line no-control-regex
|
|
604
604
|
if (!/[%\\\x00-\x1f\x7f]/.test(path) && !path.startsWith('//')) {
|
|
605
|
-
return path
|
|
605
|
+
return { path, handledProtocolRelativeURL: false }
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
const re =
|
|
609
|
-
? new RegExp(`${decodeIgnore.join('|')}`, 'gi')
|
|
610
|
-
: /%25|%5C/gi
|
|
608
|
+
const re = /%25|%5C/gi
|
|
611
609
|
let cursor = 0
|
|
612
610
|
let result = ''
|
|
613
611
|
let match
|
|
@@ -620,11 +618,13 @@ export function decodePath(path: string, decodeIgnore?: Array<string>): string {
|
|
|
620
618
|
// Prevent open redirect via protocol-relative URLs (e.g. "//evil.com")
|
|
621
619
|
// After sanitizing control characters, paths like "/\r/evil.com" become "//evil.com"
|
|
622
620
|
// Collapse leading double slashes to a single slash
|
|
621
|
+
let handledProtocolRelativeURL = false
|
|
623
622
|
if (result.startsWith('//')) {
|
|
623
|
+
handledProtocolRelativeURL = true
|
|
624
624
|
result = '/' + result.replace(/^\/+/, '')
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
-
return result
|
|
627
|
+
return { path: result, handledProtocolRelativeURL }
|
|
628
628
|
}
|
|
629
629
|
|
|
630
630
|
/**
|