@tanstack/router-core 1.155.0 → 1.157.0

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.
@@ -1,4 +1,5 @@
1
1
  import { functionalUpdate } from "./utils.js";
2
+ import { isServer } from "@tanstack/router-is-server";
2
3
  function getSafeSessionStorage() {
3
4
  try {
4
5
  if (typeof window !== "undefined" && typeof window.sessionStorage === "object") {
@@ -122,14 +123,14 @@ function restoreScroll({
122
123
  ignoreScroll = false;
123
124
  }
124
125
  function setupScrollRestoration(router, force) {
125
- if (!scrollRestorationCache && !router.isServer) {
126
+ if (!scrollRestorationCache && !(isServer ?? router.isServer)) {
126
127
  return;
127
128
  }
128
129
  const shouldScrollRestoration = force ?? router.options.scrollRestoration ?? false;
129
130
  if (shouldScrollRestoration) {
130
131
  router.isScrollRestoring = true;
131
132
  }
132
- if (router.isServer || router.isScrollRestorationSetup || !scrollRestorationCache) {
133
+ if ((isServer ?? router.isServer) || router.isScrollRestorationSetup || !scrollRestorationCache) {
133
134
  return;
134
135
  }
135
136
  router.isScrollRestorationSetup = true;
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-restoration.js","sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { functionalUpdate } from './utils'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\nimport type { HistoryLocation } from '@tanstack/history'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\nexport type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\nexport type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationCache = {\n state: ScrollRestorationByKey\n set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void\n}\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n if (\n typeof window !== 'undefined' &&\n typeof window.sessionStorage === 'object'\n ) {\n return window.sessionStorage\n }\n } catch {\n // silent\n }\n return undefined\n}\n\n/** SessionStorage key used to persist scroll restoration state. */\n/** SessionStorage key used to store scroll positions across navigations. */\n/** SessionStorage key used to store scroll positions across navigations. */\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\n\nconst throttle = (fn: (...args: Array<any>) => void, wait: number) => {\n let timeout: any\n return (...args: Array<any>) => {\n if (!timeout) {\n timeout = setTimeout(() => {\n fn(...args)\n timeout = null\n }, wait)\n }\n }\n}\n\nfunction createScrollRestorationCache(): ScrollRestorationCache | null {\n const safeSessionStorage = getSafeSessionStorage()\n if (!safeSessionStorage) {\n return null\n }\n\n const persistedState = safeSessionStorage.getItem(storageKey)\n let state: ScrollRestorationByKey = persistedState\n ? JSON.parse(persistedState)\n : {}\n\n return {\n state,\n // This setter is simply to make sure that we set the sessionStorage right\n // after the state is updated. It doesn't necessarily need to be a functional\n // update.\n set: (updater) => {\n state = functionalUpdate(updater, state) || state\n try {\n safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n } catch {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n },\n }\n}\n\n/** In-memory handle to the persisted scroll restoration cache. */\nexport const scrollRestorationCache = createScrollRestorationCache()\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\n\n/**\n * Default scroll restoration cache key: location state key or full href.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\n/** Best-effort nth-child CSS selector for a given element. */\nexport function getCssSelector(el: any): string {\n const path = []\n let parent: HTMLElement\n while ((parent = el.parentNode)) {\n path.push(\n `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,\n )\n el = parent\n }\n return `${path.reverse().join(' > ')}`.toLowerCase()\n}\n\nlet ignoreScroll = false\n\n// NOTE: This function must remain pure and not use any outside variables\n// unless they are passed in as arguments. Why? Because we need to be able to\n// toString() it into a script tag to execute as early as possible in the browser\n// during SSR. Additionally, we also call it from within the router lifecycle\nexport function restoreScroll({\n storageKey,\n key,\n behavior,\n shouldScrollRestoration,\n scrollToTopSelectors,\n location,\n}: {\n storageKey: string\n key?: string\n behavior?: ScrollToOptions['behavior']\n shouldScrollRestoration?: boolean\n scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>\n location?: HistoryLocation\n}) {\n let byKey: ScrollRestorationByKey\n\n try {\n byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n } catch (error) {\n console.error(error)\n return\n }\n\n const resolvedKey = key || window.history.state?.__TSR_key\n const elementEntries = byKey[resolvedKey]\n\n //\n ignoreScroll = true\n\n //\n scroll: {\n // If we have a cached entry for this location state,\n // we always need to prefer that over the hash scroll.\n if (\n shouldScrollRestoration &&\n elementEntries &&\n Object.keys(elementEntries).length > 0\n ) {\n for (const elementSelector in elementEntries) {\n const entry = elementEntries[elementSelector]!\n if (elementSelector === 'window') {\n window.scrollTo({\n top: entry.scrollY,\n left: entry.scrollX,\n behavior,\n })\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n element.scrollLeft = entry.scrollX\n element.scrollTop = entry.scrollY\n }\n }\n }\n\n break scroll\n }\n\n // If we don't have a cached entry for the hash,\n // Which means we've never seen this location before,\n // we need to check if there is a hash in the URL.\n // If there is, we need to scroll it's ID into view.\n const hash = (location ?? window.location).hash.split('#', 2)[1]\n\n if (hash) {\n const hashScrollIntoViewOptions =\n window.history.state?.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions) {\n const el = document.getElementById(hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n\n break scroll\n }\n\n // If there is no cached entry for the hash and there is no hash in the URL,\n // we need to scroll to the top of the page for every scrollToTop element\n const scrollOptions = { top: 0, left: 0, behavior }\n window.scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n for (const selector of scrollToTopSelectors) {\n if (selector === 'window') continue\n const element =\n typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n if (element) element.scrollTo(scrollOptions)\n }\n }\n }\n\n //\n ignoreScroll = false\n}\n\n/** Setup global listeners and hooks to support scroll restoration. */\n/** Setup global listeners and hooks to support scroll restoration. */\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n if (!scrollRestorationCache && !router.isServer) {\n return\n }\n const shouldScrollRestoration =\n force ?? router.options.scrollRestoration ?? false\n\n if (shouldScrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if (\n router.isServer ||\n router.isScrollRestorationSetup ||\n !scrollRestorationCache\n ) {\n return\n }\n\n router.isScrollRestorationSetup = true\n\n //\n ignoreScroll = false\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n\n window.history.scrollRestoration = 'manual'\n\n // // Create a MutationObserver to monitor DOM changes\n // const mutationObserver = new MutationObserver(() => {\n // ;ignoreScroll = true\n // requestAnimationFrame(() => {\n // ;ignoreScroll = false\n\n // // Attempt to restore scroll position on each dom\n // // mutation until the user scrolls. We do this\n // // because dynamic content may come in at different\n // // ticks after the initial render and we want to\n // // keep up with that content as much as possible.\n // // As soon as the user scrolls, we no longer need\n // // to attempt router.\n // // console.log('mutation observer restoreScroll')\n // restoreScroll(\n // storageKey,\n // getKey(router.state.location),\n // router.options.scrollRestorationBehavior,\n // )\n // })\n // })\n\n // const observeDom = () => {\n // // Observe changes to the entire document\n // mutationObserver.observe(document, {\n // childList: true, // Detect added or removed child nodes\n // subtree: true, // Monitor all descendants\n // characterData: true, // Detect text content changes\n // })\n // }\n\n // const unobserveDom = () => {\n // mutationObserver.disconnect()\n // }\n\n // observeDom()\n\n const onScroll = (event: Event) => {\n // unobserveDom()\n\n if (ignoreScroll || !router.isScrollRestoring) {\n return\n }\n\n let elementSelector = ''\n\n if (event.target === document || event.target === window) {\n elementSelector = 'window'\n } else {\n const attrId = (event.target as Element).getAttribute(\n 'data-scroll-restoration-id',\n )\n\n if (attrId) {\n elementSelector = `[data-scroll-restoration-id=\"${attrId}\"]`\n } else {\n elementSelector = getCssSelector(event.target)\n }\n }\n\n const restoreKey = getKey(router.state.location)\n\n scrollRestorationCache.set((state) => {\n const keyEntry = (state[restoreKey] ||= {} as ScrollRestorationByElement)\n\n const elementEntry = (keyEntry[elementSelector] ||=\n {} as ScrollRestorationEntry)\n\n if (elementSelector === 'window') {\n elementEntry.scrollX = window.scrollX || 0\n elementEntry.scrollY = window.scrollY || 0\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n elementEntry.scrollX = element.scrollLeft || 0\n elementEntry.scrollY = element.scrollTop || 0\n }\n }\n\n return state\n })\n }\n\n // Throttle the scroll event to avoid excessive updates\n if (typeof document !== 'undefined') {\n document.addEventListener('scroll', throttle(onScroll, 100), true)\n }\n\n router.subscribe('onRendered', (event) => {\n // unobserveDom()\n\n const cacheKey = getKey(event.toLocation)\n\n // If the user doesn't want to restore the scroll position,\n // we don't need to do anything.\n if (!router.resetNextScroll) {\n router.resetNextScroll = true\n return\n }\n if (typeof router.options.scrollRestoration === 'function') {\n const shouldRestore = router.options.scrollRestoration({\n location: router.latestLocation,\n })\n if (!shouldRestore) {\n return\n }\n }\n\n restoreScroll({\n storageKey,\n key: cacheKey,\n behavior: router.options.scrollRestorationBehavior,\n shouldScrollRestoration: router.isScrollRestoring,\n scrollToTopSelectors: router.options.scrollToTopSelectors,\n location: router.history.location,\n })\n\n if (router.isScrollRestoring) {\n // Mark the location as having been seen\n scrollRestorationCache.set((state) => {\n state[cacheKey] ||= {} as ScrollRestorationByElement\n\n return state\n })\n }\n })\n}\n\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific <Transitioner> components during the onResolved event.\n *\n * Provides hash scrolling for programmatic navigation when default browser handling is prevented.\n * @param router The router instance containing current location and state\n */\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific Transitioners.\n */\nexport function handleHashScroll(router: AnyRouter) {\n if (typeof document !== 'undefined' && (document as any).querySelector) {\n const hashScrollIntoViewOptions =\n router.state.location.state.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions && router.state.location.hash !== '') {\n const el = document.getElementById(router.state.location.hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n }\n}\n"],"names":["storageKey"],"mappings":"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"}
1
+ {"version":3,"file":"scroll-restoration.js","sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { functionalUpdate } from './utils'\nimport { isServer } from './isServer'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\nimport type { HistoryLocation } from '@tanstack/history'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\nexport type ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\nexport type ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationCache = {\n state: ScrollRestorationByKey\n set: (updater: NonNullableUpdater<ScrollRestorationByKey>) => void\n}\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n if (\n typeof window !== 'undefined' &&\n typeof window.sessionStorage === 'object'\n ) {\n return window.sessionStorage\n }\n } catch {\n // silent\n }\n return undefined\n}\n\n/** SessionStorage key used to persist scroll restoration state. */\n/** SessionStorage key used to store scroll positions across navigations. */\n/** SessionStorage key used to store scroll positions across navigations. */\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\n\nconst throttle = (fn: (...args: Array<any>) => void, wait: number) => {\n let timeout: any\n return (...args: Array<any>) => {\n if (!timeout) {\n timeout = setTimeout(() => {\n fn(...args)\n timeout = null\n }, wait)\n }\n }\n}\n\nfunction createScrollRestorationCache(): ScrollRestorationCache | null {\n const safeSessionStorage = getSafeSessionStorage()\n if (!safeSessionStorage) {\n return null\n }\n\n const persistedState = safeSessionStorage.getItem(storageKey)\n let state: ScrollRestorationByKey = persistedState\n ? JSON.parse(persistedState)\n : {}\n\n return {\n state,\n // This setter is simply to make sure that we set the sessionStorage right\n // after the state is updated. It doesn't necessarily need to be a functional\n // update.\n set: (updater) => {\n state = functionalUpdate(updater, state) || state\n try {\n safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n } catch {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n },\n }\n}\n\n/** In-memory handle to the persisted scroll restoration cache. */\nexport const scrollRestorationCache = createScrollRestorationCache()\n\n/**\n * The default `getKey` function for `useScrollRestoration`.\n * It returns the `key` from the location state or the `href` of the location.\n *\n * The `location.href` is used as a fallback to support the use case where the location state is not available like the initial render.\n */\n\n/**\n * Default scroll restoration cache key: location state key or full href.\n */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\n/** Best-effort nth-child CSS selector for a given element. */\nexport function getCssSelector(el: any): string {\n const path = []\n let parent: HTMLElement\n while ((parent = el.parentNode)) {\n path.push(\n `${el.tagName}:nth-child(${Array.prototype.indexOf.call(parent.children, el) + 1})`,\n )\n el = parent\n }\n return `${path.reverse().join(' > ')}`.toLowerCase()\n}\n\nlet ignoreScroll = false\n\n// NOTE: This function must remain pure and not use any outside variables\n// unless they are passed in as arguments. Why? Because we need to be able to\n// toString() it into a script tag to execute as early as possible in the browser\n// during SSR. Additionally, we also call it from within the router lifecycle\nexport function restoreScroll({\n storageKey,\n key,\n behavior,\n shouldScrollRestoration,\n scrollToTopSelectors,\n location,\n}: {\n storageKey: string\n key?: string\n behavior?: ScrollToOptions['behavior']\n shouldScrollRestoration?: boolean\n scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>\n location?: HistoryLocation\n}) {\n let byKey: ScrollRestorationByKey\n\n try {\n byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n } catch (error) {\n console.error(error)\n return\n }\n\n const resolvedKey = key || window.history.state?.__TSR_key\n const elementEntries = byKey[resolvedKey]\n\n //\n ignoreScroll = true\n\n //\n scroll: {\n // If we have a cached entry for this location state,\n // we always need to prefer that over the hash scroll.\n if (\n shouldScrollRestoration &&\n elementEntries &&\n Object.keys(elementEntries).length > 0\n ) {\n for (const elementSelector in elementEntries) {\n const entry = elementEntries[elementSelector]!\n if (elementSelector === 'window') {\n window.scrollTo({\n top: entry.scrollY,\n left: entry.scrollX,\n behavior,\n })\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n element.scrollLeft = entry.scrollX\n element.scrollTop = entry.scrollY\n }\n }\n }\n\n break scroll\n }\n\n // If we don't have a cached entry for the hash,\n // Which means we've never seen this location before,\n // we need to check if there is a hash in the URL.\n // If there is, we need to scroll it's ID into view.\n const hash = (location ?? window.location).hash.split('#', 2)[1]\n\n if (hash) {\n const hashScrollIntoViewOptions =\n window.history.state?.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions) {\n const el = document.getElementById(hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n\n break scroll\n }\n\n // If there is no cached entry for the hash and there is no hash in the URL,\n // we need to scroll to the top of the page for every scrollToTop element\n const scrollOptions = { top: 0, left: 0, behavior }\n window.scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n for (const selector of scrollToTopSelectors) {\n if (selector === 'window') continue\n const element =\n typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n if (element) element.scrollTo(scrollOptions)\n }\n }\n }\n\n //\n ignoreScroll = false\n}\n\n/** Setup global listeners and hooks to support scroll restoration. */\n/** Setup global listeners and hooks to support scroll restoration. */\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n if (!scrollRestorationCache && !(isServer ?? router.isServer)) {\n return\n }\n const shouldScrollRestoration =\n force ?? router.options.scrollRestoration ?? false\n\n if (shouldScrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if (\n (isServer ?? router.isServer) ||\n router.isScrollRestorationSetup ||\n !scrollRestorationCache\n ) {\n return\n }\n\n router.isScrollRestorationSetup = true\n\n //\n ignoreScroll = false\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n\n window.history.scrollRestoration = 'manual'\n\n // // Create a MutationObserver to monitor DOM changes\n // const mutationObserver = new MutationObserver(() => {\n // ;ignoreScroll = true\n // requestAnimationFrame(() => {\n // ;ignoreScroll = false\n\n // // Attempt to restore scroll position on each dom\n // // mutation until the user scrolls. We do this\n // // because dynamic content may come in at different\n // // ticks after the initial render and we want to\n // // keep up with that content as much as possible.\n // // As soon as the user scrolls, we no longer need\n // // to attempt router.\n // // console.log('mutation observer restoreScroll')\n // restoreScroll(\n // storageKey,\n // getKey(router.state.location),\n // router.options.scrollRestorationBehavior,\n // )\n // })\n // })\n\n // const observeDom = () => {\n // // Observe changes to the entire document\n // mutationObserver.observe(document, {\n // childList: true, // Detect added or removed child nodes\n // subtree: true, // Monitor all descendants\n // characterData: true, // Detect text content changes\n // })\n // }\n\n // const unobserveDom = () => {\n // mutationObserver.disconnect()\n // }\n\n // observeDom()\n\n const onScroll = (event: Event) => {\n // unobserveDom()\n\n if (ignoreScroll || !router.isScrollRestoring) {\n return\n }\n\n let elementSelector = ''\n\n if (event.target === document || event.target === window) {\n elementSelector = 'window'\n } else {\n const attrId = (event.target as Element).getAttribute(\n 'data-scroll-restoration-id',\n )\n\n if (attrId) {\n elementSelector = `[data-scroll-restoration-id=\"${attrId}\"]`\n } else {\n elementSelector = getCssSelector(event.target)\n }\n }\n\n const restoreKey = getKey(router.state.location)\n\n scrollRestorationCache.set((state) => {\n const keyEntry = (state[restoreKey] ||= {} as ScrollRestorationByElement)\n\n const elementEntry = (keyEntry[elementSelector] ||=\n {} as ScrollRestorationEntry)\n\n if (elementSelector === 'window') {\n elementEntry.scrollX = window.scrollX || 0\n elementEntry.scrollY = window.scrollY || 0\n } else if (elementSelector) {\n const element = document.querySelector(elementSelector)\n if (element) {\n elementEntry.scrollX = element.scrollLeft || 0\n elementEntry.scrollY = element.scrollTop || 0\n }\n }\n\n return state\n })\n }\n\n // Throttle the scroll event to avoid excessive updates\n if (typeof document !== 'undefined') {\n document.addEventListener('scroll', throttle(onScroll, 100), true)\n }\n\n router.subscribe('onRendered', (event) => {\n // unobserveDom()\n\n const cacheKey = getKey(event.toLocation)\n\n // If the user doesn't want to restore the scroll position,\n // we don't need to do anything.\n if (!router.resetNextScroll) {\n router.resetNextScroll = true\n return\n }\n if (typeof router.options.scrollRestoration === 'function') {\n const shouldRestore = router.options.scrollRestoration({\n location: router.latestLocation,\n })\n if (!shouldRestore) {\n return\n }\n }\n\n restoreScroll({\n storageKey,\n key: cacheKey,\n behavior: router.options.scrollRestorationBehavior,\n shouldScrollRestoration: router.isScrollRestoring,\n scrollToTopSelectors: router.options.scrollToTopSelectors,\n location: router.history.location,\n })\n\n if (router.isScrollRestoring) {\n // Mark the location as having been seen\n scrollRestorationCache.set((state) => {\n state[cacheKey] ||= {} as ScrollRestorationByElement\n\n return state\n })\n }\n })\n}\n\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific <Transitioner> components during the onResolved event.\n *\n * Provides hash scrolling for programmatic navigation when default browser handling is prevented.\n * @param router The router instance containing current location and state\n */\n/**\n * @private\n * Handles hash-based scrolling after navigation completes.\n * To be used in framework-specific Transitioners.\n */\nexport function handleHashScroll(router: AnyRouter) {\n if (typeof document !== 'undefined' && (document as any).querySelector) {\n const hashScrollIntoViewOptions =\n router.state.location.state.__hashScrollIntoViewOptions ?? true\n\n if (hashScrollIntoViewOptions && router.state.location.hash !== '') {\n const el = document.getElementById(router.state.location.hash)\n if (el) {\n el.scrollIntoView(hashScrollIntoViewOptions)\n }\n }\n }\n}\n"],"names":["storageKey"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.155.0",
3
+ "version": "1.157.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -70,7 +70,8 @@
70
70
  "seroval-plugins": "^1.4.2",
71
71
  "tiny-invariant": "^1.3.3",
72
72
  "tiny-warning": "^1.0.3",
73
- "@tanstack/history": "1.154.14"
73
+ "@tanstack/history": "1.154.14",
74
+ "@tanstack/router-is-server": "1.127.3"
74
75
  },
75
76
  "devDependencies": {
76
77
  "esbuild": "^0.25.0"
package/src/index.ts CHANGED
@@ -105,6 +105,7 @@ export {
105
105
  export { encode, decode } from './qss'
106
106
  export { rootRouteId } from './root'
107
107
  export type { RootRouteId } from './root'
108
+ export { isServer } from './isServer'
108
109
 
109
110
  export { BaseRoute, BaseRouteApi, BaseRootRoute } from './route'
110
111
  export type {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Static server/client detection for tree-shaking support.
3
+ *
4
+ * This file re-exports `isServer` from `@tanstack/router-is-server` which uses
5
+ * conditional exports to provide different values based on the environment:
6
+ *
7
+ * - `browser` condition → `false` (client)
8
+ * - `node`/`worker`/`deno`/`bun` → `true` (server)
9
+ * - `development` condition → `undefined` (for tests, falls back to router.isServer)
10
+ *
11
+ * The bundler resolves the correct file at build time based on export conditions,
12
+ * and since the value is a literal constant, dead code can be eliminated.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { isServer } from '@tanstack/router-core'
17
+ *
18
+ * // The ?? operator provides fallback for development/test mode
19
+ * if (isServer ?? router.isServer) {
20
+ * // Server-only code - eliminated in client bundles
21
+ * }
22
+ * ```
23
+ */
24
+ export { isServer } from '@tanstack/router-is-server'
@@ -4,6 +4,7 @@ import { createControlledPromise, isPromise } from './utils'
4
4
  import { isNotFound } from './not-found'
5
5
  import { rootRouteId } from './root'
6
6
  import { isRedirect } from './redirect'
7
+ import { isServer } from './isServer'
7
8
  import type { NotFoundError } from './not-found'
8
9
  import type { ParsedLocation } from './location'
9
10
  import type {
@@ -169,11 +170,11 @@ const shouldSkipLoader = (
169
170
  ): boolean => {
170
171
  const match = inner.router.getMatch(matchId)!
171
172
  // upon hydration, we skip the loader if the match has been dehydrated on the server
172
- if (!inner.router.isServer && match._nonReactive.dehydrated) {
173
+ if (!(isServer ?? inner.router.isServer) && match._nonReactive.dehydrated) {
173
174
  return true
174
175
  }
175
176
 
176
- if (inner.router.isServer && match.ssr === false) {
177
+ if ((isServer ?? inner.router.isServer) && match.ssr === false) {
177
178
  return true
178
179
  }
179
180
 
@@ -306,7 +307,7 @@ const setupPendingTimeout = (
306
307
  route.options.pendingMs ?? inner.router.options.defaultPendingMs
307
308
  const shouldPending = !!(
308
309
  inner.onReady &&
309
- !inner.router.isServer &&
310
+ !(isServer ?? inner.router.isServer) &&
310
311
  !resolvePreload(inner, matchId) &&
311
312
  (route.options.loader ||
312
313
  route.options.beforeLoad ||
@@ -520,7 +521,7 @@ const handleBeforeLoad = (
520
521
 
521
522
  const serverSsr = () => {
522
523
  // on the server, determine whether SSR the current match or not
523
- if (inner.router.isServer) {
524
+ if (isServer ?? inner.router.isServer) {
524
525
  const maybePromise = isBeforeLoadSsr(inner, matchId, index, route)
525
526
  if (isPromise(maybePromise)) return maybePromise.then(queueExecution)
526
527
  }
@@ -635,7 +636,7 @@ const runLoader = async (
635
636
 
636
637
  // Actually run the loader and handle the result
637
638
  try {
638
- if (!inner.router.isServer || match.ssr === true) {
639
+ if (!(isServer ?? inner.router.isServer) || match.ssr === true) {
639
640
  loadRouteChunk(route)
640
641
  }
641
642
 
@@ -759,7 +760,7 @@ const loadRouteMatch = async (
759
760
  const route = inner.router.looseRoutesById[routeId]!
760
761
 
761
762
  if (shouldSkipLoader(inner, matchId)) {
762
- if (inner.router.isServer) {
763
+ if (isServer ?? inner.router.isServer) {
763
764
  return inner.router.getMatch(matchId)!
764
765
  }
765
766
  } else {
@@ -879,7 +880,7 @@ export async function loadMatches(arg: {
879
880
  // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
880
881
  // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
881
882
  if (
882
- !inner.router.isServer &&
883
+ !(isServer ?? inner.router.isServer) &&
883
884
  inner.router.state.matches.some((d) => d._forcePending)
884
885
  ) {
885
886
  triggerOnReady(inner)
@@ -778,6 +778,17 @@ export function trimPathRight(path: string) {
778
778
  return path === '/' ? path : path.replace(/\/{1,}$/, '')
779
779
  }
780
780
 
781
+ export interface ProcessRouteTreeResult<
782
+ TRouteLike extends Extract<RouteLike, { fullPath: string }> & { id: string },
783
+ > {
784
+ /** Should be considered a black box, needs to be provided to all matching functions in this module. */
785
+ processedTree: ProcessedTree<TRouteLike, any, any>
786
+ /** A lookup map of routes by their unique IDs. */
787
+ routesById: Record<string, TRouteLike>
788
+ /** A lookup map of routes by their trimmed full paths. */
789
+ routesByPath: Record<string, TRouteLike>
790
+ }
791
+
781
792
  /**
782
793
  * Processes a route tree into a segment trie for efficient path matching.
783
794
  * Also builds lookup maps for routes by ID and by trimmed full path.
@@ -791,14 +802,7 @@ export function processRouteTree<
791
802
  caseSensitive: boolean = false,
792
803
  /** Optional callback invoked for each route during processing. */
793
804
  initRoute?: (route: TRouteLike, index: number) => void,
794
- ): {
795
- /** Should be considered a black box, needs to be provided to all matching functions in this module. */
796
- processedTree: ProcessedTree<TRouteLike, any, any>
797
- /** A lookup map of routes by their unique IDs. */
798
- routesById: Record<string, TRouteLike>
799
- /** A lookup map of routes by their trimmed full paths. */
800
- routesByPath: Record<string, TRouteLike>
801
- } {
805
+ ): ProcessRouteTreeResult<TRouteLike> {
802
806
  const segmentTree = createStaticNode<TRouteLike>(routeTree.fullPath)
803
807
  const data = new Uint16Array(6)
804
808
  const routesById = {} as Record<string, TRouteLike>
package/src/router.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  trimPath,
26
26
  trimPathRight,
27
27
  } from './path'
28
+ import { isServer } from './isServer'
28
29
  import { createLRUCache } from './lru-cache'
29
30
  import { isNotFound } from './not-found'
30
31
  import { setupScrollRestoration } from './scroll-restoration'
@@ -38,7 +39,11 @@ import {
38
39
  executeRewriteOutput,
39
40
  rewriteBasepath,
40
41
  } from './rewrite'
41
- import type { ProcessedTree } from './new-process-route-tree'
42
+ import type { LRUCache } from './lru-cache'
43
+ import type {
44
+ ProcessRouteTreeResult,
45
+ ProcessedTree,
46
+ } from './new-process-route-tree'
42
47
  import type { SearchParser, SearchSerializer } from './searchParams'
43
48
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
44
49
  import type {
@@ -589,7 +594,6 @@ export type SubscribeFn = <TType extends keyof RouterEvents>(
589
594
  export interface MatchRoutesOpts {
590
595
  preload?: boolean
591
596
  throwOnError?: boolean
592
- _buildLocation?: boolean
593
597
  dest?: BuildNextOptions
594
598
  }
595
599
 
@@ -873,6 +877,17 @@ export type CreateRouterFn = <
873
877
  TDehydrated
874
878
  >
875
879
 
880
+ declare global {
881
+ // eslint-disable-next-line no-var
882
+ var __TSR_CACHE__:
883
+ | {
884
+ routeTree: AnyRoute
885
+ processRouteTreeResult: ProcessRouteTreeResult<AnyRoute>
886
+ resolvePathCache: LRUCache<string, string>
887
+ }
888
+ | undefined
889
+ }
890
+
876
891
  /**
877
892
  * Core, framework-agnostic router engine that powers TanStack Router.
878
893
  *
@@ -923,6 +938,7 @@ export class RouterCore<
923
938
  routesById!: RoutesById<TRouteTree>
924
939
  routesByPath!: RoutesByPath<TRouteTree>
925
940
  processedTree!: ProcessedTree<TRouteTree, any, any>
941
+ resolvePathCache!: LRUCache<string, string>
926
942
  isServer!: boolean
927
943
  pathParamsDecoder?: (encoded: string) => string
928
944
 
@@ -1003,7 +1019,7 @@ export class RouterCore<
1003
1019
  (this.options.history && this.options.history !== this.history)
1004
1020
  ) {
1005
1021
  if (!this.options.history) {
1006
- if (!this.isServer) {
1022
+ if (!(isServer ?? this.isServer)) {
1007
1023
  this.history = createBrowserHistory() as TRouterHistory
1008
1024
  }
1009
1025
  } else {
@@ -1013,7 +1029,11 @@ export class RouterCore<
1013
1029
 
1014
1030
  this.origin = this.options.origin
1015
1031
  if (!this.origin) {
1016
- if (!this.isServer && window?.origin && window.origin !== 'null') {
1032
+ if (
1033
+ !(isServer ?? this.isServer) &&
1034
+ window?.origin &&
1035
+ window.origin !== 'null'
1036
+ ) {
1017
1037
  this.origin = window.origin
1018
1038
  } else {
1019
1039
  // fallback for the server, can be overridden by calling router.update({origin}) on the server
@@ -1027,7 +1047,31 @@ export class RouterCore<
1027
1047
 
1028
1048
  if (this.options.routeTree !== this.routeTree) {
1029
1049
  this.routeTree = this.options.routeTree as TRouteTree
1030
- this.buildRouteTree()
1050
+ let processRouteTreeResult: ProcessRouteTreeResult<TRouteTree>
1051
+ if (
1052
+ (isServer ?? this.isServer) &&
1053
+ globalThis.__TSR_CACHE__ &&
1054
+ globalThis.__TSR_CACHE__.routeTree === this.routeTree
1055
+ ) {
1056
+ const cached = globalThis.__TSR_CACHE__
1057
+ this.resolvePathCache = cached.resolvePathCache
1058
+ processRouteTreeResult = cached.processRouteTreeResult as any
1059
+ } else {
1060
+ this.resolvePathCache = createLRUCache(1000)
1061
+ processRouteTreeResult = this.buildRouteTree()
1062
+ // only cache if nothing else is cached yet
1063
+ if (
1064
+ (isServer ?? this.isServer) &&
1065
+ globalThis.__TSR_CACHE__ === undefined
1066
+ ) {
1067
+ globalThis.__TSR_CACHE__ = {
1068
+ routeTree: this.routeTree,
1069
+ processRouteTreeResult: processRouteTreeResult as any,
1070
+ resolvePathCache: this.resolvePathCache,
1071
+ }
1072
+ }
1073
+ }
1074
+ this.setRoutes(processRouteTreeResult)
1031
1075
  }
1032
1076
 
1033
1077
  if (!this.__store && this.latestLocation) {
@@ -1110,7 +1154,7 @@ export class RouterCore<
1110
1154
  }
1111
1155
 
1112
1156
  buildRouteTree = () => {
1113
- const { routesById, routesByPath, processedTree } = processRouteTree(
1157
+ const result = processRouteTree(
1114
1158
  this.routeTree,
1115
1159
  this.options.caseSensitive,
1116
1160
  (route, i) => {
@@ -1120,9 +1164,17 @@ export class RouterCore<
1120
1164
  },
1121
1165
  )
1122
1166
  if (this.options.routeMasks) {
1123
- processRouteMasks(this.options.routeMasks, processedTree)
1167
+ processRouteMasks(this.options.routeMasks, result.processedTree)
1124
1168
  }
1125
1169
 
1170
+ return result
1171
+ }
1172
+
1173
+ setRoutes({
1174
+ routesById,
1175
+ routesByPath,
1176
+ processedTree,
1177
+ }: ProcessRouteTreeResult<TRouteTree>) {
1126
1178
  this.routesById = routesById as RoutesById<TRouteTree>
1127
1179
  this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
1128
1180
  this.processedTree = processedTree
@@ -1222,8 +1274,6 @@ export class RouterCore<
1222
1274
  return location
1223
1275
  }
1224
1276
 
1225
- resolvePathCache = createLRUCache<string, string>(1000)
1226
-
1227
1277
  /** Resolve a path against the router basepath and trailing-slash policy. */
1228
1278
  resolvePathWithBase = (from: string, path: string) => {
1229
1279
  const resolvedPath = resolvePath({
@@ -1390,35 +1440,19 @@ export class RouterCore<
1390
1440
  let paramsError: unknown = undefined
1391
1441
 
1392
1442
  if (!existingMatch) {
1393
- if (route.options.skipRouteOnParseError) {
1394
- for (const key in usedParams) {
1395
- if (key in parsedParams!) {
1396
- strictParams[key] = parsedParams![key]
1397
- }
1443
+ try {
1444
+ extractStrictParams(route, usedParams, parsedParams!, strictParams)
1445
+ } catch (err: any) {
1446
+ if (isNotFound(err) || isRedirect(err)) {
1447
+ paramsError = err
1448
+ } else {
1449
+ paramsError = new PathParamError(err.message, {
1450
+ cause: err,
1451
+ })
1398
1452
  }
1399
- } else {
1400
- const strictParseParams =
1401
- route.options.params?.parse ?? route.options.parseParams
1402
-
1403
- if (strictParseParams) {
1404
- try {
1405
- Object.assign(
1406
- strictParams,
1407
- strictParseParams(strictParams as Record<string, string>),
1408
- )
1409
- } catch (err: any) {
1410
- if (isNotFound(err) || isRedirect(err)) {
1411
- paramsError = err
1412
- } else {
1413
- paramsError = new PathParamError(err.message, {
1414
- cause: err,
1415
- })
1416
- }
1417
1453
 
1418
- if (opts?.throwOnError) {
1419
- throw paramsError
1420
- }
1421
- }
1454
+ if (opts?.throwOnError) {
1455
+ throw paramsError
1422
1456
  }
1423
1457
  }
1424
1458
  }
@@ -1453,7 +1487,7 @@ export class RouterCore<
1453
1487
 
1454
1488
  match = {
1455
1489
  id: matchId,
1456
- ssr: this.isServer ? undefined : route.options.ssr,
1490
+ ssr: (isServer ?? this.isServer) ? undefined : route.options.ssr,
1457
1491
  index,
1458
1492
  routeId: route.id,
1459
1493
  params: previousMatch
@@ -1519,7 +1553,7 @@ export class RouterCore<
1519
1553
 
1520
1554
  // only execute `context` if we are not calling from router.buildLocation
1521
1555
 
1522
- if (!existingMatch && opts?._buildLocation !== true) {
1556
+ if (!existingMatch) {
1523
1557
  const parentMatch = matches[index - 1]
1524
1558
  const parentContext = getParentContext(parentMatch)
1525
1559
 
@@ -1563,6 +1597,80 @@ export class RouterCore<
1563
1597
  })
1564
1598
  }
1565
1599
 
1600
+ /**
1601
+ * Lightweight route matching for buildLocation.
1602
+ * Only computes fullPath, accumulated search, and params - skipping expensive
1603
+ * operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
1604
+ */
1605
+ private matchRoutesLightweight(location: ParsedLocation): {
1606
+ matchedRoutes: ReadonlyArray<AnyRoute>
1607
+ fullPath: string
1608
+ search: Record<string, unknown>
1609
+ params: Record<string, unknown>
1610
+ } {
1611
+ const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes(
1612
+ location.pathname,
1613
+ )
1614
+ const lastRoute = last(matchedRoutes)!
1615
+
1616
+ // I don't know if we should run the full search middleware chain, or just validateSearch
1617
+ // // Accumulate search validation through the route chain
1618
+ // const accumulatedSearch: Record<string, unknown> = applySearchMiddleware({
1619
+ // search: { ...location.search },
1620
+ // dest: location,
1621
+ // destRoutes: matchedRoutes,
1622
+ // _includeValidateSearch: true,
1623
+ // })
1624
+
1625
+ // Accumulate search validation through route chain
1626
+ const accumulatedSearch = { ...location.search }
1627
+ for (const route of matchedRoutes) {
1628
+ try {
1629
+ Object.assign(
1630
+ accumulatedSearch,
1631
+ validateSearch(route.options.validateSearch, accumulatedSearch),
1632
+ )
1633
+ } catch {
1634
+ // Ignore errors, we're not actually routing
1635
+ }
1636
+ }
1637
+
1638
+ // Determine params: reuse from state if possible, otherwise parse
1639
+ const lastStateMatch = last(this.state.matches)
1640
+ const canReuseParams =
1641
+ lastStateMatch &&
1642
+ lastStateMatch.routeId === lastRoute.id &&
1643
+ location.pathname === this.state.location.pathname
1644
+
1645
+ let params: Record<string, unknown>
1646
+ if (canReuseParams) {
1647
+ params = lastStateMatch.params
1648
+ } else {
1649
+ // Parse params through the route chain
1650
+ const strictParams: Record<string, unknown> = { ...routeParams }
1651
+ for (const route of matchedRoutes) {
1652
+ try {
1653
+ extractStrictParams(
1654
+ route,
1655
+ routeParams,
1656
+ parsedParams ?? {},
1657
+ strictParams,
1658
+ )
1659
+ } catch {
1660
+ // Ignore errors, we're not actually routing
1661
+ }
1662
+ }
1663
+ params = strictParams
1664
+ }
1665
+
1666
+ return {
1667
+ matchedRoutes,
1668
+ fullPath: lastRoute.fullPath,
1669
+ search: accumulatedSearch,
1670
+ params,
1671
+ }
1672
+ }
1673
+
1566
1674
  cancelMatch = (id: string) => {
1567
1675
  const match = this.getMatch(id)
1568
1676
 
@@ -1607,13 +1715,9 @@ export class RouterCore<
1607
1715
  const currentLocation =
1608
1716
  dest._fromLocation || this.pendingBuiltLocation || this.latestLocation
1609
1717
 
1610
- const allCurrentLocationMatches = this.matchRoutes(currentLocation, {
1611
- _buildLocation: true,
1612
- })
1613
-
1614
- // Now let's find the starting pathname
1615
- // This should default to the current location if no from is provided
1616
- const lastMatch = last(allCurrentLocationMatches)!
1718
+ // Use lightweight matching - only computes what buildLocation needs
1719
+ // (fullPath, search, params) without creating full match objects
1720
+ const lightweightResult = this.matchRoutesLightweight(currentLocation)
1617
1721
 
1618
1722
  // check that from path exists in the current route tree
1619
1723
  // do this check only on navigations during test or development
@@ -1624,12 +1728,12 @@ export class RouterCore<
1624
1728
  ) {
1625
1729
  const allFromMatches = this.getMatchedRoutes(dest.from).matchedRoutes
1626
1730
 
1627
- const matchedFrom = findLast(allCurrentLocationMatches, (d) => {
1731
+ const matchedFrom = findLast(lightweightResult.matchedRoutes, (d) => {
1628
1732
  return comparePaths(d.fullPath, dest.from!)
1629
1733
  })
1630
1734
 
1631
1735
  const matchedCurrent = findLast(allFromMatches, (d) => {
1632
- return comparePaths(d.fullPath, lastMatch.fullPath)
1736
+ return comparePaths(d.fullPath, lightweightResult.fullPath)
1633
1737
  })
1634
1738
 
1635
1739
  // for from to be invalid it shouldn't just be unmatched to currentLocation
@@ -1642,15 +1746,15 @@ export class RouterCore<
1642
1746
  const defaultedFromPath =
1643
1747
  dest.unsafeRelative === 'path'
1644
1748
  ? currentLocation.pathname
1645
- : (dest.from ?? lastMatch.fullPath)
1749
+ : (dest.from ?? lightweightResult.fullPath)
1646
1750
 
1647
1751
  // ensure this includes the basePath if set
1648
1752
  const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1649
1753
 
1650
1754
  // From search should always use the current location
1651
- const fromSearch = lastMatch.search
1755
+ const fromSearch = lightweightResult.search
1652
1756
  // Same with params. It can't hurt to provide as many as possible
1653
- const fromParams = { ...lastMatch.params }
1757
+ const fromParams = { ...lightweightResult.params }
1654
1758
 
1655
1759
  // Resolve the next to
1656
1760
  // ensure this includes the basePath if set
@@ -2119,7 +2223,7 @@ export class RouterCore<
2119
2223
  this.cancelMatches()
2120
2224
  this.updateLatestLocation()
2121
2225
 
2122
- if (this.isServer) {
2226
+ if (isServer ?? this.isServer) {
2123
2227
  // for SPAs on the initial load, this is handled by the Transitioner
2124
2228
  const nextLocation = this.buildLocation({
2125
2229
  to: this.latestLocation.pathname,
@@ -2269,7 +2373,7 @@ export class RouterCore<
2269
2373
  } catch (err) {
2270
2374
  if (isRedirect(err)) {
2271
2375
  redirect = err
2272
- if (!this.isServer) {
2376
+ if (!(isServer ?? this.isServer)) {
2273
2377
  this.navigate({
2274
2378
  ...redirect.options,
2275
2379
  replace: true,
@@ -2799,7 +2903,7 @@ function applySearchMiddleware({
2799
2903
  _includeValidateSearch,
2800
2904
  }: {
2801
2905
  search: any
2802
- dest: BuildNextOptions
2906
+ dest: { search?: unknown }
2803
2907
  destRoutes: ReadonlyArray<AnyRoute>
2804
2908
  _includeValidateSearch: boolean | undefined
2805
2909
  }) {
@@ -2934,3 +3038,25 @@ function findGlobalNotFoundRouteId(
2934
3038
  }
2935
3039
  return rootRouteId
2936
3040
  }
3041
+
3042
+ function extractStrictParams(
3043
+ route: AnyRoute,
3044
+ referenceParams: Record<string, unknown>,
3045
+ parsedParams: Record<string, unknown>,
3046
+ accumulatedParams: Record<string, unknown>,
3047
+ ) {
3048
+ const parseParams = route.options.params?.parse ?? route.options.parseParams
3049
+ if (parseParams) {
3050
+ if (route.options.skipRouteOnParseError) {
3051
+ // Use pre-parsed params from route matching for skipRouteOnParseError routes
3052
+ for (const key in referenceParams) {
3053
+ if (key in parsedParams) {
3054
+ accumulatedParams[key] = parsedParams[key]
3055
+ }
3056
+ }
3057
+ } else {
3058
+ const result = parseParams(accumulatedParams as Record<string, string>)
3059
+ Object.assign(accumulatedParams, result)
3060
+ }
3061
+ }
3062
+ }