@tanstack/router-core 1.129.7 → 1.129.9

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.
@@ -65,7 +65,7 @@ function restoreScroll(storageKey2, key, behavior, shouldScrollRestoration, scro
65
65
  const elementEntries = byKey[resolvedKey];
66
66
  ignoreScroll = true;
67
67
  (() => {
68
- if (shouldScrollRestoration && elementEntries) {
68
+ if (shouldScrollRestoration && elementEntries && Object.keys(elementEntries).length > 0) {
69
69
  for (const elementSelector in elementEntries) {
70
70
  const entry = elementEntries[elementSelector];
71
71
  if (elementSelector === "window") {
@@ -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'\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 return undefined\n }\n return undefined\n}\n\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 | undefined {\n const safeSessionStorage = getSafeSessionStorage()\n if (!safeSessionStorage) {\n return undefined\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 safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n ),\n }\n}\n\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\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\nexport function getCssSelector(el: any): string {\n const path = []\n let parent\n while ((parent = el.parentNode)) {\n path.unshift(\n `${el.tagName}:nth-child(${([].indexOf as any).call(parent.children, el) + 1})`,\n )\n el = parent\n }\n return `${path.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: string,\n key: string | undefined,\n behavior: ScrollToOptions['behavior'] | undefined,\n shouldScrollRestoration: boolean | undefined,\n scrollToTopSelectors:\n | Array<string | (() => Element | null | undefined)>\n | undefined,\n) {\n let byKey: ScrollRestorationByKey\n\n try {\n byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n } catch (error: any) {\n console.error(error)\n return\n }\n\n const resolvedKey = key || window.history.state?.key\n const elementEntries = byKey[resolvedKey]\n\n //\n ignoreScroll = true\n\n //\n ;(() => {\n // If we have a cached entry for this location state,\n // we always need to prefer that over the hash scroll.\n if (shouldScrollRestoration && elementEntries) {\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 return\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 = window.location.hash.split('#')[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 return\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 ;[\n 'window',\n ...(scrollToTopSelectors?.filter((d) => d !== 'window') ?? []),\n ].forEach((selector) => {\n const element =\n selector === 'window'\n ? window\n : typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n if (element) {\n element.scrollTo({\n top: 0,\n left: 0,\n behavior,\n })\n }\n })\n })()\n\n //\n ignoreScroll = false\n}\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n if (scrollRestorationCache === undefined) {\n return\n }\n const shouldScrollRestoration =\n force ?? router.options.scrollRestoration ?? false\n\n if (shouldScrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if (typeof document === 'undefined' || router.isScrollRestorationSetup) {\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] =\n state[restoreKey] || ({} as ScrollRestorationByElement))\n\n const elementEntry = (keyEntry[elementSelector] =\n keyEntry[elementSelector] || ({} 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\n restoreScroll(\n storageKey,\n cacheKey,\n router.options.scrollRestorationBehavior || undefined,\n router.isScrollRestoring || undefined,\n router.options.scrollToTopSelectors || undefined,\n )\n\n if (router.isScrollRestoring) {\n // Mark the location as having been seen\n scrollRestorationCache.set((state) => {\n state[cacheKey] = state[cacheKey] || ({} as ScrollRestorationByElement)\n\n return state\n })\n }\n })\n}\n\n/**\n * @internal\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 */\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 type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\nimport type { NonNullableUpdater } from './utils'\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 return undefined\n }\n return undefined\n}\n\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 | undefined {\n const safeSessionStorage = getSafeSessionStorage()\n if (!safeSessionStorage) {\n return undefined\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 safeSessionStorage.setItem(storageKey, JSON.stringify(state))\n ),\n }\n}\n\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\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\nexport function getCssSelector(el: any): string {\n const path = []\n let parent\n while ((parent = el.parentNode)) {\n path.unshift(\n `${el.tagName}:nth-child(${([].indexOf as any).call(parent.children, el) + 1})`,\n )\n el = parent\n }\n return `${path.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: string,\n key: string | undefined,\n behavior: ScrollToOptions['behavior'] | undefined,\n shouldScrollRestoration: boolean | undefined,\n scrollToTopSelectors:\n | Array<string | (() => Element | null | undefined)>\n | undefined,\n) {\n let byKey: ScrollRestorationByKey\n\n try {\n byKey = JSON.parse(sessionStorage.getItem(storageKey) || '{}')\n } catch (error: any) {\n console.error(error)\n return\n }\n\n const resolvedKey = key || window.history.state?.key\n const elementEntries = byKey[resolvedKey]\n\n //\n ignoreScroll = true\n\n //\n ;(() => {\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 return\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 = window.location.hash.split('#')[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 return\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 ;[\n 'window',\n ...(scrollToTopSelectors?.filter((d) => d !== 'window') ?? []),\n ].forEach((selector) => {\n const element =\n selector === 'window'\n ? window\n : typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n if (element) {\n element.scrollTo({\n top: 0,\n left: 0,\n behavior,\n })\n }\n })\n })()\n\n //\n ignoreScroll = false\n}\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n if (scrollRestorationCache === undefined) {\n return\n }\n const shouldScrollRestoration =\n force ?? router.options.scrollRestoration ?? false\n\n if (shouldScrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if (typeof document === 'undefined' || router.isScrollRestorationSetup) {\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] =\n state[restoreKey] || ({} as ScrollRestorationByElement))\n\n const elementEntry = (keyEntry[elementSelector] =\n keyEntry[elementSelector] || ({} 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\n restoreScroll(\n storageKey,\n cacheKey,\n router.options.scrollRestorationBehavior || undefined,\n router.isScrollRestoring || undefined,\n router.options.scrollToTopSelectors || undefined,\n )\n\n if (router.isScrollRestoring) {\n // Mark the location as having been seen\n scrollRestorationCache.set((state) => {\n state[cacheKey] = state[cacheKey] || ({} as ScrollRestorationByElement)\n\n return state\n })\n }\n })\n}\n\n/**\n * @internal\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 */\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.129.7",
3
+ "version": "1.129.9",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -0,0 +1,68 @@
1
+ export type LRUCache<TKey, TValue> = {
2
+ get: (key: TKey) => TValue | undefined
3
+ set: (key: TKey, value: TValue) => void
4
+ }
5
+
6
+ export function createLRUCache<TKey, TValue>(
7
+ max: number,
8
+ ): LRUCache<TKey, TValue> {
9
+ type Node = { prev?: Node; next?: Node; key: TKey; value: TValue }
10
+ const cache = new Map<TKey, Node>()
11
+ let oldest: Node | undefined
12
+ let newest: Node | undefined
13
+
14
+ const touch = (entry: Node) => {
15
+ if (!entry.next) return
16
+ if (!entry.prev) {
17
+ entry.next.prev = undefined
18
+ oldest = entry.next
19
+ entry.next = undefined
20
+ if (newest) {
21
+ entry.prev = newest
22
+ newest.next = entry
23
+ }
24
+ } else {
25
+ entry.prev.next = entry.next
26
+ entry.next.prev = entry.prev
27
+ entry.next = undefined
28
+ if (newest) {
29
+ newest.next = entry
30
+ entry.prev = newest
31
+ }
32
+ }
33
+ newest = entry
34
+ }
35
+
36
+ return {
37
+ get(key) {
38
+ const entry = cache.get(key)
39
+ if (!entry) return undefined
40
+ touch(entry)
41
+ return entry.value
42
+ },
43
+ set(key, value) {
44
+ if (cache.size >= max && oldest) {
45
+ const toDelete = oldest
46
+ cache.delete(toDelete.key)
47
+ if (toDelete.next) {
48
+ oldest = toDelete.next
49
+ toDelete.next.prev = undefined
50
+ }
51
+ if (toDelete === newest) {
52
+ newest = undefined
53
+ }
54
+ }
55
+ const existing = cache.get(key)
56
+ if (existing) {
57
+ existing.value = value
58
+ touch(existing)
59
+ } else {
60
+ const entry: Node = { key, value, prev: newest }
61
+ if (newest) newest.next = entry
62
+ newest = entry
63
+ if (!oldest) oldest = entry
64
+ cache.set(key, entry)
65
+ }
66
+ },
67
+ }
68
+ }
package/src/path.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { last } from './utils'
2
+ import type { LRUCache } from './lru-cache'
2
3
  import type { MatchLocation } from './RouterProvider'
3
4
  import type { AnyPathParams } from './route'
4
5
 
@@ -101,6 +102,7 @@ interface ResolvePathOptions {
101
102
  to: string
102
103
  trailingSlash?: 'always' | 'never' | 'preserve'
103
104
  caseSensitive?: boolean
105
+ parseCache?: ParsePathnameCache
104
106
  }
105
107
 
106
108
  function segmentToString(segment: Segment): string {
@@ -154,12 +156,13 @@ export function resolvePath({
154
156
  to,
155
157
  trailingSlash = 'never',
156
158
  caseSensitive,
159
+ parseCache,
157
160
  }: ResolvePathOptions) {
158
161
  base = removeBasepath(basepath, base, caseSensitive)
159
162
  to = removeBasepath(basepath, to, caseSensitive)
160
163
 
161
- let baseSegments = parsePathname(base).slice()
162
- const toSegments = parsePathname(to)
164
+ let baseSegments = parsePathname(base, parseCache).slice()
165
+ const toSegments = parsePathname(to, parseCache)
163
166
 
164
167
  if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
165
168
  baseSegments.pop()
@@ -202,6 +205,19 @@ export function resolvePath({
202
205
  return joined
203
206
  }
204
207
 
208
+ export type ParsePathnameCache = LRUCache<string, ReadonlyArray<Segment>>
209
+ export const parsePathname = (
210
+ pathname?: string,
211
+ cache?: ParsePathnameCache,
212
+ ): ReadonlyArray<Segment> => {
213
+ if (!pathname) return []
214
+ const cached = cache?.get(pathname)
215
+ if (cached) return cached
216
+ const parsed = baseParsePathname(pathname)
217
+ cache?.set(pathname, parsed)
218
+ return parsed
219
+ }
220
+
205
221
  const PARAM_RE = /^\$.{1,}$/ // $paramName
206
222
  const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
207
223
  const OPTIONAL_PARAM_W_CURLY_BRACES_RE =
@@ -227,11 +243,7 @@ const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
227
243
  * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
228
244
  * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
229
245
  */
230
- export function parsePathname(pathname?: string): ReadonlyArray<Segment> {
231
- if (!pathname) {
232
- return []
233
- }
234
-
246
+ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
235
247
  pathname = cleanPath(pathname)
236
248
 
237
249
  const segments: Array<Segment> = []
@@ -348,6 +360,7 @@ interface InterpolatePathOptions {
348
360
  leaveParams?: boolean
349
361
  // Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params
350
362
  decodeCharMap?: Map<string, string>
363
+ parseCache?: ParsePathnameCache
351
364
  }
352
365
 
353
366
  type InterPolatePathResult = {
@@ -361,8 +374,9 @@ export function interpolatePath({
361
374
  leaveWildcards,
362
375
  leaveParams,
363
376
  decodeCharMap,
377
+ parseCache,
364
378
  }: InterpolatePathOptions): InterPolatePathResult {
365
- const interpolatedPathSegments = parsePathname(path)
379
+ const interpolatedPathSegments = parsePathname(path, parseCache)
366
380
 
367
381
  function encodeParam(key: string): any {
368
382
  const value = params[key]
@@ -480,8 +494,14 @@ export function matchPathname(
480
494
  basepath: string,
481
495
  currentPathname: string,
482
496
  matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
497
+ parseCache?: ParsePathnameCache,
483
498
  ): AnyPathParams | undefined {
484
- const pathParams = matchByPath(basepath, currentPathname, matchLocation)
499
+ const pathParams = matchByPath(
500
+ basepath,
501
+ currentPathname,
502
+ matchLocation,
503
+ parseCache,
504
+ )
485
505
  // const searchMatched = matchBySearch(location.search, matchLocation)
486
506
 
487
507
  if (matchLocation.to && !pathParams) {
@@ -540,6 +560,7 @@ export function matchByPath(
540
560
  fuzzy,
541
561
  caseSensitive,
542
562
  }: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
563
+ parseCache?: ParsePathnameCache,
543
564
  ): Record<string, string> | undefined {
544
565
  // check basepath first
545
566
  if (basepath !== '/' && !from.startsWith(basepath)) {
@@ -551,8 +572,14 @@ export function matchByPath(
551
572
  to = removeBasepath(basepath, `${to ?? '$'}`, caseSensitive)
552
573
 
553
574
  // Parse the from and to
554
- const baseSegments = parsePathname(from.startsWith('/') ? from : `/${from}`)
555
- const routeSegments = parsePathname(to.startsWith('/') ? to : `/${to}`)
575
+ const baseSegments = parsePathname(
576
+ from.startsWith('/') ? from : `/${from}`,
577
+ parseCache,
578
+ )
579
+ const routeSegments = parsePathname(
580
+ to.startsWith('/') ? to : `/${to}`,
581
+ parseCache,
582
+ )
556
583
 
557
584
  const params: Record<string, string> = {}
558
585
 
package/src/router.ts CHANGED
@@ -33,7 +33,8 @@ import { setupScrollRestoration } from './scroll-restoration'
33
33
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
34
34
  import { rootRouteId } from './root'
35
35
  import { isRedirect, redirect } from './redirect'
36
- import type { Segment } from './path'
36
+ import { createLRUCache } from './lru-cache'
37
+ import type { ParsePathnameCache, Segment } from './path'
37
38
  import type { SearchParser, SearchSerializer } from './searchParams'
38
39
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
39
40
  import type {
@@ -1014,6 +1015,7 @@ export class RouterCore<
1014
1015
  to: cleanPath(path),
1015
1016
  trailingSlash: this.options.trailingSlash,
1016
1017
  caseSensitive: this.options.caseSensitive,
1018
+ parseCache: this.parsePathnameCache,
1017
1019
  })
1018
1020
  return resolvedPath
1019
1021
  }
@@ -1195,6 +1197,7 @@ export class RouterCore<
1195
1197
  params: routeParams,
1196
1198
  leaveWildcards: true,
1197
1199
  decodeCharMap: this.pathParamsDecodeCharMap,
1200
+ parseCache: this.parsePathnameCache,
1198
1201
  }).interpolatedPath + loaderDepsHash
1199
1202
 
1200
1203
  // Waste not, want not. If we already have a match for this route,
@@ -1333,6 +1336,9 @@ export class RouterCore<
1333
1336
  return matches
1334
1337
  }
1335
1338
 
1339
+ /** a cache for `parsePathname` */
1340
+ private parsePathnameCache: ParsePathnameCache = createLRUCache(1000)
1341
+
1336
1342
  getMatchedRoutes: GetMatchRoutesFn = (
1337
1343
  pathname: string,
1338
1344
  routePathname: string | undefined,
@@ -1345,6 +1351,7 @@ export class RouterCore<
1345
1351
  routesByPath: this.routesByPath,
1346
1352
  routesById: this.routesById,
1347
1353
  flatRoutes: this.flatRoutes,
1354
+ parseCache: this.parsePathnameCache,
1348
1355
  })
1349
1356
  }
1350
1357
 
@@ -1452,6 +1459,7 @@ export class RouterCore<
1452
1459
  const interpolatedNextTo = interpolatePath({
1453
1460
  path: nextTo,
1454
1461
  params: nextParams ?? {},
1462
+ parseCache: this.parsePathnameCache,
1455
1463
  }).interpolatedPath
1456
1464
 
1457
1465
  const destRoutes = this.matchRoutes(
@@ -1484,6 +1492,7 @@ export class RouterCore<
1484
1492
  leaveWildcards: false,
1485
1493
  leaveParams: opts.leaveParams,
1486
1494
  decodeCharMap: this.pathParamsDecodeCharMap,
1495
+ parseCache: this.parsePathnameCache,
1487
1496
  }).interpolatedPath
1488
1497
 
1489
1498
  // Resolve the next search
@@ -1567,11 +1576,16 @@ export class RouterCore<
1567
1576
  let params = {}
1568
1577
 
1569
1578
  const foundMask = this.options.routeMasks?.find((d) => {
1570
- const match = matchPathname(this.basepath, next.pathname, {
1571
- to: d.from,
1572
- caseSensitive: false,
1573
- fuzzy: false,
1574
- })
1579
+ const match = matchPathname(
1580
+ this.basepath,
1581
+ next.pathname,
1582
+ {
1583
+ to: d.from,
1584
+ caseSensitive: false,
1585
+ fuzzy: false,
1586
+ },
1587
+ this.parsePathnameCache,
1588
+ )
1575
1589
 
1576
1590
  if (match) {
1577
1591
  params = match
@@ -2971,10 +2985,15 @@ export class RouterCore<
2971
2985
  ? this.latestLocation
2972
2986
  : this.state.resolvedLocation || this.state.location
2973
2987
 
2974
- const match = matchPathname(this.basepath, baseLocation.pathname, {
2975
- ...opts,
2976
- to: next.pathname,
2977
- }) as any
2988
+ const match = matchPathname(
2989
+ this.basepath,
2990
+ baseLocation.pathname,
2991
+ {
2992
+ ...opts,
2993
+ to: next.pathname,
2994
+ },
2995
+ this.parsePathnameCache,
2996
+ ) as any
2978
2997
 
2979
2998
  if (!match) {
2980
2999
  return false
@@ -3368,6 +3387,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3368
3387
  routesByPath,
3369
3388
  routesById,
3370
3389
  flatRoutes,
3390
+ parseCache,
3371
3391
  }: {
3372
3392
  pathname: string
3373
3393
  routePathname?: string
@@ -3376,16 +3396,22 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3376
3396
  routesByPath: Record<string, TRouteLike>
3377
3397
  routesById: Record<string, TRouteLike>
3378
3398
  flatRoutes: Array<TRouteLike>
3399
+ parseCache?: ParsePathnameCache
3379
3400
  }) {
3380
3401
  let routeParams: Record<string, string> = {}
3381
3402
  const trimmedPath = trimPathRight(pathname)
3382
3403
  const getMatchedParams = (route: TRouteLike) => {
3383
- const result = matchPathname(basepath, trimmedPath, {
3384
- to: route.fullPath,
3385
- caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3386
- // we need fuzzy matching for `notFoundMode: 'fuzzy'`
3387
- fuzzy: true,
3388
- })
3404
+ const result = matchPathname(
3405
+ basepath,
3406
+ trimmedPath,
3407
+ {
3408
+ to: route.fullPath,
3409
+ caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3410
+ // we need fuzzy matching for `notFoundMode: 'fuzzy'`
3411
+ fuzzy: true,
3412
+ },
3413
+ parseCache,
3414
+ )
3389
3415
  return result
3390
3416
  }
3391
3417
 
@@ -128,7 +128,11 @@ export function restoreScroll(
128
128
  ;(() => {
129
129
  // If we have a cached entry for this location state,
130
130
  // we always need to prefer that over the hash scroll.
131
- if (shouldScrollRestoration && elementEntries) {
131
+ if (
132
+ shouldScrollRestoration &&
133
+ elementEntries &&
134
+ Object.keys(elementEntries).length > 0
135
+ ) {
132
136
  for (const elementSelector in elementEntries) {
133
137
  const entry = elementEntries[elementSelector]!
134
138
  if (elementSelector === 'window') {