@tanstack/router-core 1.171.11 → 1.171.13

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.
@@ -76,10 +76,10 @@ function getScrollToTopElements(scrollToTopSelectors) {
76
76
  return elements;
77
77
  }
78
78
  function setupScrollRestoration(router, force) {
79
- if (force ?? router.options.scrollRestoration) router.isScrollRestoring = true;
80
- if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) return;
81
- router.isScrollRestorationSetup = true;
82
- ignoreScroll = false;
79
+ const shouldSetupScrollRestoration = force ?? router.options.scrollRestoration;
80
+ const scroll = router._scroll;
81
+ if (shouldSetupScrollRestoration) scroll.restoring = true;
82
+ if (isServer ?? router.isServer) return;
83
83
  const getKey = router.options.getScrollRestorationKey || defaultGetScrollRestorationKey;
84
84
  const trackedScrollEntries = /* @__PURE__ */ new Map();
85
85
  const setTrackedScrollEntry = (target, scrollX, scrollY) => {
@@ -88,9 +88,8 @@ function setupScrollRestoration(router, force) {
88
88
  entry.scrollY = scrollY;
89
89
  trackedScrollEntries.set(target, entry);
90
90
  };
91
- history.scrollRestoration = "manual";
92
91
  const onScroll = (event) => {
93
- if (ignoreScroll || !router.isScrollRestoring) return;
92
+ if (ignoreScroll || !scroll.restoring) return;
94
93
  if (event.target === document) setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY);
95
94
  else {
96
95
  const target = event.target;
@@ -98,31 +97,38 @@ function setupScrollRestoration(router, force) {
98
97
  }
99
98
  };
100
99
  const snapshotCurrentScrollTargets = (restoreKey) => {
101
- if (!router.isScrollRestoring) return;
100
+ if (!scroll.restoring) return;
102
101
  const keyEntry = scrollRestorationCache[restoreKey] ||= {};
103
102
  for (const [target, position] of trackedScrollEntries) if (target === windowScrollTarget) keyEntry[windowScrollTarget] = position;
104
103
  else if (target.isConnected) keyEntry[getScrollRestorationSelector(target)] = position;
105
104
  };
106
- document.addEventListener("scroll", onScroll, true);
107
- router.subscribe("onBeforeLoad", (event) => {
108
- if (event.fromLocation) snapshotCurrentScrollTargets(getKey(event.fromLocation));
109
- trackedScrollEntries.clear();
110
- });
111
- addEventListener("pagehide", () => {
112
- snapshotCurrentScrollTargets(getKey(router.stores.resolvedLocation.get() ?? router.stores.location.get()));
113
- persistScrollRestorationCache();
114
- });
105
+ if (shouldSetupScrollRestoration && !scroll.restoration) {
106
+ scroll.restoration = true;
107
+ ignoreScroll = false;
108
+ history.scrollRestoration = "manual";
109
+ document.addEventListener("scroll", onScroll, true);
110
+ router.subscribe("onBeforeLoad", (event) => {
111
+ if (event.fromLocation) snapshotCurrentScrollTargets(getKey(event.fromLocation));
112
+ trackedScrollEntries.clear();
113
+ });
114
+ addEventListener("pagehide", () => {
115
+ snapshotCurrentScrollTargets(getKey(router.stores.resolvedLocation.get() ?? router.stores.location.get()));
116
+ persistScrollRestorationCache();
117
+ });
118
+ }
119
+ if (scroll.reset) return;
120
+ scroll.reset = true;
115
121
  router.subscribe("onRendered", (event) => {
116
122
  const behavior = router.options.scrollRestorationBehavior;
117
123
  const scrollToTopSelectors = router.options.scrollToTopSelectors;
118
- const shouldResetScroll = router.resetNextScroll;
124
+ const shouldResetScroll = scroll.next;
119
125
  let scrollToTopElements;
120
126
  trackedScrollEntries.clear();
121
- if (!shouldResetScroll) router.resetNextScroll = true;
127
+ if (!shouldResetScroll) scroll.next = true;
122
128
  if (typeof router.options.scrollRestoration === "function" && !router.options.scrollRestoration({ location: router.latestLocation })) return;
123
129
  const cacheKey = getKey(event.toLocation);
124
130
  const fromCacheKey = event.fromLocation && getKey(event.fromLocation);
125
- if (router.isScrollRestoring && fromCacheKey && fromCacheKey !== cacheKey) {
131
+ if (scroll.restoring && fromCacheKey && fromCacheKey !== cacheKey) {
126
132
  const fromElementEntries = scrollRestorationCache[fromCacheKey];
127
133
  if (fromElementEntries) {
128
134
  let toElementEntries = scrollRestorationCache[cacheKey];
@@ -150,7 +156,7 @@ function setupScrollRestoration(router, force) {
150
156
  if (shouldResetScroll) {
151
157
  const action = locationHistoryActions.get(event.toLocation);
152
158
  const skipWindowRestore = hash && hashScrollIntoViewOptions && (action === "PUSH" || action === "REPLACE");
153
- const elementEntries = router.isScrollRestoring ? scrollRestorationCache[cacheKey] : void 0;
159
+ const elementEntries = scroll.restoring ? scrollRestorationCache[cacheKey] : void 0;
154
160
  if (elementEntries) for (const elementSelector in elementEntries) {
155
161
  const { scrollX, scrollY } = elementEntries[elementSelector];
156
162
  if (elementSelector === windowScrollTarget) {
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-restoration.js","names":[],"sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { locationHistoryActions } from './router'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\ntype ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\ntype ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n // Accessing sessionStorage itself can throw SecurityError in locked-down\n // contexts, e.g. sandboxed/opaque origins or blocked storage policies.\n return sessionStorage\n } catch {\n return\n }\n}\n\n// SessionStorage key used to store scroll positions across navigations.\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\nconst safeSessionStorage = getSafeSessionStorage()\n\nfunction createScrollRestorationCache() {\n try {\n return JSON.parse(\n safeSessionStorage?.getItem(storageKey) || '{}',\n ) as ScrollRestorationByKey\n } catch {\n // ignore invalid session storage payloads\n return {}\n }\n}\n\nfunction persistScrollRestorationCache() {\n try {\n safeSessionStorage?.setItem(\n storageKey,\n JSON.stringify(scrollRestorationCache),\n )\n } catch {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n }\n}\n\nconst scrollRestorationCache = /* @__PURE__ */ createScrollRestorationCache()\nconst scrollRestorationIdAttribute = 'data-scroll-restoration-id'\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 */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\nfunction getScrollRestorationSelector(element: Element): string {\n const attrId = element.getAttribute(scrollRestorationIdAttribute)\n if (attrId) {\n return `[${scrollRestorationIdAttribute}=\"${attrId}\"]`\n }\n\n let selector = ''\n let el: any = element\n let parent: HTMLElement\n\n while ((parent = el.parentNode)) {\n let index = 1\n let sibling = el\n while ((sibling = sibling.previousElementSibling)) {\n index++\n }\n\n const part = `${el.localName}:nth-child(${index})`\n selector = selector ? `${part} > ${selector}` : part\n el = parent\n }\n\n return selector\n}\n\nexport function getElementScrollRestorationEntry(\n router: AnyRouter,\n options: (\n | {\n id: string\n getElement?: () => Window | Element | undefined | null\n }\n | {\n id?: string\n getElement: () => Window | Element | undefined | null\n }\n ) & {\n getKey?: (location: ParsedLocation) => string\n },\n): ScrollRestorationEntry | undefined {\n const getKey = options.getKey || defaultGetScrollRestorationKey\n const restoreKey = getKey(router.latestLocation)\n const entries = scrollRestorationCache[restoreKey]\n\n if (!entries) {\n return\n }\n\n if (options.id) {\n return entries[`[${scrollRestorationIdAttribute}=\"${options.id}\"]`]\n }\n\n const element = options.getElement?.()\n if (!element) {\n return\n }\n\n return entries[\n element === window\n ? windowScrollTarget\n : getScrollRestorationSelector(element as Element)\n ]\n}\n\nlet ignoreScroll = false\nconst windowScrollTarget = 'window'\ntype ScrollTarget = typeof windowScrollTarget | Element\n\nfunction getElement(selector: string | (() => Element | null | undefined)) {\n try {\n return typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n } catch {}\n return\n}\n\nfunction getScrollToTopElements(\n scrollToTopSelectors: NonNullable<\n AnyRouter['options']['scrollToTopSelectors']\n >,\n): Array<Element> {\n const elements: Array<Element> = []\n\n for (const selector of scrollToTopSelectors) {\n if (selector === windowScrollTarget) {\n continue\n }\n\n const element = getElement(selector)\n if (element) {\n elements.push(element)\n }\n }\n\n return elements\n}\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n // Keep hash/top scrolling active even when sessionStorage is unavailable.\n\n if (force ?? router.options.scrollRestoration) {\n router.isScrollRestoring = true\n }\n\n if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) {\n return\n }\n\n router.isScrollRestorationSetup = true\n ignoreScroll = false\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()\n const setTrackedScrollEntry = (\n target: ScrollTarget,\n scrollX: number,\n scrollY: number,\n ) => {\n const entry =\n trackedScrollEntries.get(target) || ({} as ScrollRestorationEntry)\n entry.scrollX = scrollX\n entry.scrollY = scrollY\n trackedScrollEntries.set(target, entry)\n }\n\n history.scrollRestoration = 'manual'\n\n const onScroll = (event: Event) => {\n if (ignoreScroll || !router.isScrollRestoring) {\n return\n }\n\n if (event.target === document) {\n setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY)\n } else {\n const target = event.target as Element\n setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop)\n }\n }\n\n // Snapshot the current page's tracked scroll targets before navigation or unload.\n const snapshotCurrentScrollTargets = (restoreKey: string) => {\n if (!router.isScrollRestoring) {\n return\n }\n\n const keyEntry = (scrollRestorationCache[restoreKey] ||=\n {} as ScrollRestorationByElement)\n\n for (const [target, position] of trackedScrollEntries) {\n if (target === windowScrollTarget) {\n keyEntry[windowScrollTarget] = position\n } else if (target.isConnected) {\n keyEntry[getScrollRestorationSelector(target)] = position\n }\n }\n }\n\n document.addEventListener('scroll', onScroll, true)\n router.subscribe('onBeforeLoad', (event) => {\n if (event.fromLocation) {\n snapshotCurrentScrollTargets(getKey(event.fromLocation))\n }\n trackedScrollEntries.clear()\n })\n addEventListener('pagehide', () => {\n snapshotCurrentScrollTargets(\n getKey(\n router.stores.resolvedLocation.get() ?? router.stores.location.get(),\n ),\n )\n persistScrollRestorationCache()\n })\n\n // Restore destination scroll after the new route has rendered.\n router.subscribe('onRendered', (event) => {\n const behavior = router.options.scrollRestorationBehavior\n const scrollToTopSelectors = router.options.scrollToTopSelectors\n const shouldResetScroll = router.resetNextScroll\n let scrollToTopElements: Array<Element> | undefined\n trackedScrollEntries.clear()\n\n if (!shouldResetScroll) {\n router.resetNextScroll = true\n }\n\n if (\n typeof router.options.scrollRestoration === 'function' &&\n !router.options.scrollRestoration({ location: router.latestLocation })\n ) {\n return\n }\n\n const cacheKey = getKey(event.toLocation)\n const fromCacheKey = event.fromLocation && getKey(event.fromLocation)\n\n if (router.isScrollRestoring && fromCacheKey && fromCacheKey !== cacheKey) {\n const fromElementEntries = scrollRestorationCache[fromCacheKey]\n\n if (fromElementEntries) {\n let toElementEntries = scrollRestorationCache[cacheKey]\n\n for (const elementSelector in fromElementEntries) {\n if (elementSelector === windowScrollTarget) {\n if (shouldResetScroll) {\n continue\n }\n } else {\n const element = getElement(elementSelector)\n if (!element) {\n continue\n }\n\n if (shouldResetScroll && scrollToTopSelectors) {\n scrollToTopElements ??=\n getScrollToTopElements(scrollToTopSelectors)\n if (scrollToTopElements.includes(element)) {\n continue\n }\n }\n }\n\n if (!toElementEntries) {\n toElementEntries = scrollRestorationCache[cacheKey] =\n {} as ScrollRestorationByElement\n }\n\n toElementEntries[elementSelector] ??=\n fromElementEntries[elementSelector]!\n }\n }\n }\n\n ignoreScroll = true\n\n try {\n const hash = event.toLocation.hash\n const hashScrollIntoViewOptions =\n event.toLocation.state.__hashScrollIntoViewOptions ?? true\n let windowRestored = false\n\n if (shouldResetScroll) {\n const action = locationHistoryActions.get(event.toLocation)\n const skipWindowRestore =\n hash &&\n hashScrollIntoViewOptions &&\n (action === 'PUSH' || action === 'REPLACE')\n\n const elementEntries = router.isScrollRestoring\n ? scrollRestorationCache[cacheKey]\n : undefined\n\n if (elementEntries) {\n for (const elementSelector in elementEntries) {\n const { scrollX, scrollY } = elementEntries[elementSelector]!\n\n if (elementSelector === windowScrollTarget) {\n if (skipWindowRestore) {\n continue\n }\n\n scrollTo({\n top: scrollY,\n left: scrollX,\n behavior,\n })\n windowRestored = true\n } else {\n const element = getElement(elementSelector)\n if (element) {\n element.scrollLeft = scrollX\n element.scrollTop = scrollY\n }\n }\n }\n }\n\n if (!windowRestored && !hash) {\n const scrollOptions = {\n top: 0,\n left: 0,\n behavior,\n }\n\n scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)\n for (const element of scrollToTopElements) {\n element.scrollTo(scrollOptions)\n }\n }\n }\n }\n\n if (!windowRestored && hash && hashScrollIntoViewOptions) {\n document.getElementById(hash)?.scrollIntoView(hashScrollIntoViewOptions)\n }\n } finally {\n ignoreScroll = false\n }\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"scroll-restoration.js","names":[],"sources":["../../src/scroll-restoration.ts"],"sourcesContent":["import { isServer } from '@tanstack/router-core/isServer'\nimport { locationHistoryActions } from './router'\nimport type { AnyRouter } from './router'\nimport type { ParsedLocation } from './location'\n\nexport type ScrollRestorationEntry = { scrollX: number; scrollY: number }\n\ntype ScrollRestorationByElement = Record<string, ScrollRestorationEntry>\n\ntype ScrollRestorationByKey = Record<string, ScrollRestorationByElement>\n\nexport type ScrollRestorationOptions = {\n getKey?: (location: ParsedLocation) => string\n scrollBehavior?: ScrollToOptions['behavior']\n}\n\nfunction getSafeSessionStorage() {\n try {\n // Accessing sessionStorage itself can throw SecurityError in locked-down\n // contexts, e.g. sandboxed/opaque origins or blocked storage policies.\n return sessionStorage\n } catch {\n return\n }\n}\n\n// SessionStorage key used to store scroll positions across navigations.\nexport const storageKey = 'tsr-scroll-restoration-v1_3'\nconst safeSessionStorage = getSafeSessionStorage()\n\nfunction createScrollRestorationCache() {\n try {\n return JSON.parse(\n safeSessionStorage?.getItem(storageKey) || '{}',\n ) as ScrollRestorationByKey\n } catch {\n // ignore invalid session storage payloads\n return {}\n }\n}\n\nfunction persistScrollRestorationCache() {\n try {\n safeSessionStorage?.setItem(\n storageKey,\n JSON.stringify(scrollRestorationCache),\n )\n } catch {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[ts-router] Could not persist scroll restoration state to sessionStorage.',\n )\n }\n }\n}\n\nconst scrollRestorationCache = /* @__PURE__ */ createScrollRestorationCache()\nconst scrollRestorationIdAttribute = 'data-scroll-restoration-id'\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 */\nexport const defaultGetScrollRestorationKey = (location: ParsedLocation) => {\n return location.state.__TSR_key! || location.href\n}\n\nfunction getScrollRestorationSelector(element: Element): string {\n const attrId = element.getAttribute(scrollRestorationIdAttribute)\n if (attrId) {\n return `[${scrollRestorationIdAttribute}=\"${attrId}\"]`\n }\n\n let selector = ''\n let el: any = element\n let parent: HTMLElement\n\n while ((parent = el.parentNode)) {\n let index = 1\n let sibling = el\n while ((sibling = sibling.previousElementSibling)) {\n index++\n }\n\n const part = `${el.localName}:nth-child(${index})`\n selector = selector ? `${part} > ${selector}` : part\n el = parent\n }\n\n return selector\n}\n\nexport function getElementScrollRestorationEntry(\n router: AnyRouter,\n options: (\n | {\n id: string\n getElement?: () => Window | Element | undefined | null\n }\n | {\n id?: string\n getElement: () => Window | Element | undefined | null\n }\n ) & {\n getKey?: (location: ParsedLocation) => string\n },\n): ScrollRestorationEntry | undefined {\n const getKey = options.getKey || defaultGetScrollRestorationKey\n const restoreKey = getKey(router.latestLocation)\n const entries = scrollRestorationCache[restoreKey]\n\n if (!entries) {\n return\n }\n\n if (options.id) {\n return entries[`[${scrollRestorationIdAttribute}=\"${options.id}\"]`]\n }\n\n const element = options.getElement?.()\n if (!element) {\n return\n }\n\n return entries[\n element === window\n ? windowScrollTarget\n : getScrollRestorationSelector(element as Element)\n ]\n}\n\nlet ignoreScroll = false\nconst windowScrollTarget = 'window'\ntype ScrollTarget = typeof windowScrollTarget | Element\n\nfunction getElement(selector: string | (() => Element | null | undefined)) {\n try {\n return typeof selector === 'function'\n ? selector()\n : document.querySelector(selector)\n } catch {}\n return\n}\n\nfunction getScrollToTopElements(\n scrollToTopSelectors: NonNullable<\n AnyRouter['options']['scrollToTopSelectors']\n >,\n): Array<Element> {\n const elements: Array<Element> = []\n\n for (const selector of scrollToTopSelectors) {\n if (selector === windowScrollTarget) {\n continue\n }\n\n const element = getElement(selector)\n if (element) {\n elements.push(element)\n }\n }\n\n return elements\n}\n\nexport function setupScrollRestoration(router: AnyRouter, force?: boolean) {\n // Keep hash/top scrolling active even when sessionStorage is unavailable.\n const shouldSetupScrollRestoration = force ?? router.options.scrollRestoration\n const scroll = router._scroll\n\n if (shouldSetupScrollRestoration) {\n scroll.restoring = true\n }\n\n if (isServer ?? router.isServer) {\n return\n }\n\n const getKey =\n router.options.getScrollRestorationKey || defaultGetScrollRestorationKey\n const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()\n const setTrackedScrollEntry = (\n target: ScrollTarget,\n scrollX: number,\n scrollY: number,\n ) => {\n const entry =\n trackedScrollEntries.get(target) || ({} as ScrollRestorationEntry)\n entry.scrollX = scrollX\n entry.scrollY = scrollY\n trackedScrollEntries.set(target, entry)\n }\n\n const onScroll = (event: Event) => {\n if (ignoreScroll || !scroll.restoring) {\n return\n }\n\n if (event.target === document) {\n setTrackedScrollEntry(windowScrollTarget, scrollX, scrollY)\n } else {\n const target = event.target as Element\n setTrackedScrollEntry(target, target.scrollLeft, target.scrollTop)\n }\n }\n\n // Snapshot the current page's tracked scroll targets before navigation or unload.\n const snapshotCurrentScrollTargets = (restoreKey: string) => {\n if (!scroll.restoring) {\n return\n }\n\n const keyEntry = (scrollRestorationCache[restoreKey] ||=\n {} as ScrollRestorationByElement)\n\n for (const [target, position] of trackedScrollEntries) {\n if (target === windowScrollTarget) {\n keyEntry[windowScrollTarget] = position\n } else if (target.isConnected) {\n keyEntry[getScrollRestorationSelector(target)] = position\n }\n }\n }\n\n if (shouldSetupScrollRestoration && !scroll.restoration) {\n scroll.restoration = true\n ignoreScroll = false\n\n history.scrollRestoration = 'manual'\n\n document.addEventListener('scroll', onScroll, true)\n router.subscribe('onBeforeLoad', (event) => {\n if (event.fromLocation) {\n snapshotCurrentScrollTargets(getKey(event.fromLocation))\n }\n trackedScrollEntries.clear()\n })\n addEventListener('pagehide', () => {\n snapshotCurrentScrollTargets(\n getKey(\n router.stores.resolvedLocation.get() ?? router.stores.location.get(),\n ),\n )\n persistScrollRestorationCache()\n })\n }\n\n if (scroll.reset) {\n return\n }\n\n scroll.reset = true\n\n // Restore destination scroll after the new route has rendered.\n router.subscribe('onRendered', (event) => {\n const behavior = router.options.scrollRestorationBehavior\n const scrollToTopSelectors = router.options.scrollToTopSelectors\n const shouldResetScroll = scroll.next\n let scrollToTopElements: Array<Element> | undefined\n trackedScrollEntries.clear()\n\n if (!shouldResetScroll) {\n scroll.next = true\n }\n\n if (\n typeof router.options.scrollRestoration === 'function' &&\n !router.options.scrollRestoration({ location: router.latestLocation })\n ) {\n return\n }\n\n const cacheKey = getKey(event.toLocation)\n const fromCacheKey = event.fromLocation && getKey(event.fromLocation)\n\n if (scroll.restoring && fromCacheKey && fromCacheKey !== cacheKey) {\n const fromElementEntries = scrollRestorationCache[fromCacheKey]\n\n if (fromElementEntries) {\n let toElementEntries = scrollRestorationCache[cacheKey]\n\n for (const elementSelector in fromElementEntries) {\n if (elementSelector === windowScrollTarget) {\n if (shouldResetScroll) {\n continue\n }\n } else {\n const element = getElement(elementSelector)\n if (!element) {\n continue\n }\n\n if (shouldResetScroll && scrollToTopSelectors) {\n scrollToTopElements ??=\n getScrollToTopElements(scrollToTopSelectors)\n if (scrollToTopElements.includes(element)) {\n continue\n }\n }\n }\n\n if (!toElementEntries) {\n toElementEntries = scrollRestorationCache[cacheKey] =\n {} as ScrollRestorationByElement\n }\n\n toElementEntries[elementSelector] ??=\n fromElementEntries[elementSelector]!\n }\n }\n }\n\n ignoreScroll = true\n\n try {\n const hash = event.toLocation.hash\n const hashScrollIntoViewOptions =\n event.toLocation.state.__hashScrollIntoViewOptions ?? true\n let windowRestored = false\n\n if (shouldResetScroll) {\n const action = locationHistoryActions.get(event.toLocation)\n const skipWindowRestore =\n hash &&\n hashScrollIntoViewOptions &&\n (action === 'PUSH' || action === 'REPLACE')\n\n const elementEntries = scroll.restoring\n ? scrollRestorationCache[cacheKey]\n : undefined\n\n if (elementEntries) {\n for (const elementSelector in elementEntries) {\n const { scrollX, scrollY } = elementEntries[elementSelector]!\n\n if (elementSelector === windowScrollTarget) {\n if (skipWindowRestore) {\n continue\n }\n\n scrollTo({\n top: scrollY,\n left: scrollX,\n behavior,\n })\n windowRestored = true\n } else {\n const element = getElement(elementSelector)\n if (element) {\n element.scrollLeft = scrollX\n element.scrollTop = scrollY\n }\n }\n }\n }\n\n if (!windowRestored && !hash) {\n const scrollOptions = {\n top: 0,\n left: 0,\n behavior,\n }\n\n scrollTo(scrollOptions)\n if (scrollToTopSelectors) {\n scrollToTopElements ??= getScrollToTopElements(scrollToTopSelectors)\n for (const element of scrollToTopElements) {\n element.scrollTo(scrollOptions)\n }\n }\n }\n }\n\n if (!windowRestored && hash && hashScrollIntoViewOptions) {\n document.getElementById(hash)?.scrollIntoView(hashScrollIntoViewOptions)\n }\n } finally {\n ignoreScroll = false\n }\n })\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { deepEqual } from "./utils.js";
1
+ import { deepEqual, hasOwn } from "./utils.js";
2
2
  //#region src/searchMiddleware.ts
3
3
  /**
4
4
  * Retain specified search params across navigations.
@@ -19,15 +19,17 @@ function retainSearchParams(keys) {
19
19
  ...resultSearch
20
20
  };
21
21
  const removed = meta.removed;
22
- for (const key of removed?.keys() || []) if (deepEqual(search[key], removed.get(key))) delete copy[key];
22
+ const explicit = meta.explicit;
23
+ for (const key of removed?.keys() || []) if (explicit && hasOwn.call(explicit, key) || deepEqual(search[key], removed.get(key))) delete copy[key];
23
24
  for (const key of meta.removedAny || []) delete copy[key];
24
- for (const key of meta.defaulted?.keys() || []) if (key in search && !meta.removedAny?.has(key) && !(meta.removed?.has(key) && deepEqual(search[key], meta.removed.get(key)))) copy[key] = search[key];
25
+ for (const key of meta.defaulted?.keys() || []) if (key in search && !meta.removedAny?.has(key) && !(meta.removed?.has(key) && (explicit && hasOwn.call(explicit, key) || deepEqual(search[key], meta.removed.get(key))))) copy[key] = search[key];
25
26
  return copy;
26
27
  }
27
28
  const copy = { ...resultSearch };
29
+ const explicit = meta.explicit;
28
30
  for (const key of keys) {
29
31
  const stringKey = key;
30
- if (!(meta.removedAny?.has(stringKey) || meta.removed?.has(stringKey) && deepEqual(search[key], meta.removed.get(stringKey))) && (!(key in copy) || key in search && meta.defaulted?.has(stringKey) && deepEqual(copy[key], meta.defaulted.get(stringKey)))) copy[key] = search[key];
32
+ if (!(meta.removedAny?.has(stringKey) || meta.removed?.has(stringKey) && (explicit && hasOwn.call(explicit, stringKey) || deepEqual(search[key], meta.removed.get(stringKey)))) && (!(key in copy) || key in search && meta.defaulted?.has(stringKey) && deepEqual(copy[key], meta.defaulted.get(stringKey)))) copy[key] = search[key];
31
33
  }
32
34
  return copy;
33
35
  };
@@ -1 +1 @@
1
- {"version":3,"file":"searchMiddleware.js","names":[],"sources":["../../src/searchMiddleware.ts"],"sourcesContent":["import { deepEqual } from './utils'\nimport type { NoInfer, PickOptional } from './utils'\nimport type {\n SearchMiddleware,\n SearchMiddlewareContext,\n SearchMiddlewareMeta,\n} from './route'\nimport type { IsRequiredParams } from './link'\n\ntype SearchMiddlewareNextWithMeta<TSearchSchema> = (\n newSearch: TSearchSchema,\n collectMeta: true,\n) => { search: TSearchSchema; meta: SearchMiddlewareMeta }\n\n/**\n * Retain specified search params across navigations.\n *\n * If `keys` is `true`, retain all current params. Otherwise, copy only the\n * listed keys from the current search into the next search.\n *\n * @param keys `true` to retain all, or a list of keys to retain.\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/retainSearchParamsFunction\n */\nexport function retainSearchParams<TSearchSchema extends object>(\n keys: Array<keyof TSearchSchema> | true,\n): SearchMiddleware<TSearchSchema> {\n return ({ search, next }) => {\n const { search: resultSearch, meta } = (\n next as unknown as SearchMiddlewareNextWithMeta<TSearchSchema>\n )(search, true)\n\n if (keys === true) {\n const copy = { ...search, ...resultSearch }\n const removed = meta.removed\n for (const key of removed?.keys() || []) {\n if (deepEqual(search[key as keyof TSearchSchema], removed!.get(key))) {\n delete copy[key as keyof TSearchSchema]\n }\n }\n for (const key of meta.removedAny || []) {\n delete copy[key as keyof TSearchSchema]\n }\n for (const key of meta.defaulted?.keys() || []) {\n if (\n key in search &&\n !meta.removedAny?.has(key) &&\n !(\n meta.removed?.has(key) &&\n deepEqual(search[key as keyof TSearchSchema], meta.removed.get(key))\n )\n ) {\n copy[key as keyof TSearchSchema] = search[key as keyof TSearchSchema]\n }\n }\n return copy\n }\n\n const copy = { ...resultSearch }\n // add missing keys from search to copy\n for (const key of keys) {\n const stringKey = key as string\n const removed =\n meta.removedAny?.has(stringKey) ||\n (meta.removed?.has(stringKey) &&\n deepEqual(search[key], meta.removed.get(stringKey)))\n if (\n !removed &&\n (!(key in copy) ||\n (key in search &&\n meta.defaulted?.has(stringKey) &&\n deepEqual(copy[key], meta.defaulted.get(stringKey))))\n ) {\n copy[key] = search[key]\n }\n }\n return copy\n }\n}\n\n/**\n * Remove optional or default-valued search params from navigations.\n *\n * - Pass `true` (only if there are no required search params) to strip all.\n * - Pass an array to always remove those optional keys.\n * - Pass an object of default values; keys equal (deeply) to the defaults are removed.\n *\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/stripSearchParamsFunction\n */\nexport function stripSearchParams<\n TSearchSchema,\n TOptionalProps = PickOptional<NoInfer<TSearchSchema>>,\n const TValues = Partial<NoInfer<TSearchSchema>> | Array<keyof TOptionalProps>,\n const TInput = IsRequiredParams<TSearchSchema> extends never\n ? TValues | true\n : TValues,\n>(input: NoInfer<TInput>): SearchMiddleware<TSearchSchema> {\n return (({ search, next, meta }: SearchMiddlewareContext<TSearchSchema>) => {\n if (input === true) {\n Object.keys(search as object).forEach((key) => {\n if (meta) {\n ;(meta.removedAny ||= new Set()).add(key)\n }\n })\n return {}\n }\n const nextResult = next(search)\n const result = { ...nextResult } as Record<string, unknown>\n if (Array.isArray(input)) {\n input.forEach((key) => {\n delete result[key as string]\n if (meta) {\n ;(meta.removedAny ||= new Set()).add(key as string)\n }\n })\n } else {\n Object.entries(input as Record<string, unknown>).forEach(\n ([key, value]) => {\n if (deepEqual(result[key], value)) {\n delete result[key]\n if (meta) {\n ;(meta.removed ||= new Map()).set(key, value)\n }\n }\n },\n )\n }\n return result as any\n }) as SearchMiddleware<TSearchSchema>\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,SAAgB,mBACd,MACiC;CACjC,QAAQ,EAAE,QAAQ,WAAW;EAC3B,MAAM,EAAE,QAAQ,cAAc,SAC5B,KACA,QAAQ,IAAI;EAEd,IAAI,SAAS,MAAM;GACjB,MAAM,OAAO;IAAE,GAAG;IAAQ,GAAG;GAAa;GAC1C,MAAM,UAAU,KAAK;GACrB,KAAK,MAAM,OAAO,SAAS,KAAK,KAAK,CAAC,GACpC,IAAI,UAAU,OAAO,MAA6B,QAAS,IAAI,GAAG,CAAC,GACjE,OAAO,KAAK;GAGhB,KAAK,MAAM,OAAO,KAAK,cAAc,CAAC,GACpC,OAAO,KAAK;GAEd,KAAK,MAAM,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,GAC3C,IACE,OAAO,UACP,CAAC,KAAK,YAAY,IAAI,GAAG,KACzB,EACE,KAAK,SAAS,IAAI,GAAG,KACrB,UAAU,OAAO,MAA6B,KAAK,QAAQ,IAAI,GAAG,CAAC,IAGrE,KAAK,OAA8B,OAAO;GAG9C,OAAO;EACT;EAEA,MAAM,OAAO,EAAE,GAAG,aAAa;EAE/B,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,YAAY;GAKlB,IACE,EAJA,KAAK,YAAY,IAAI,SAAS,KAC7B,KAAK,SAAS,IAAI,SAAS,KAC1B,UAAU,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAC,OAGnD,EAAE,OAAO,SACP,OAAO,UACN,KAAK,WAAW,IAAI,SAAS,KAC7B,UAAU,KAAK,MAAM,KAAK,UAAU,IAAI,SAAS,CAAC,IAEtD,KAAK,OAAO,OAAO;EAEvB;EACA,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,kBAOd,OAAyD;CACzD,SAAS,EAAE,QAAQ,MAAM,WAAmD;EAC1E,IAAI,UAAU,MAAM;GAClB,OAAO,KAAK,MAAgB,EAAE,SAAS,QAAQ;IAC7C,IAAI,MACD,CAAC,KAAK,+BAAe,IAAI,IAAI,GAAG,IAAI,GAAG;GAE5C,CAAC;GACD,OAAO,CAAC;EACV;EAEA,MAAM,SAAS,EAAE,GADE,KAAK,MACJ,EAAW;EAC/B,IAAI,MAAM,QAAQ,KAAK,GACrB,MAAM,SAAS,QAAQ;GACrB,OAAO,OAAO;GACd,IAAI,MACD,CAAC,KAAK,+BAAe,IAAI,IAAI,GAAG,IAAI,GAAa;EAEtD,CAAC;OAED,OAAO,QAAQ,KAAgC,EAAE,SAC9C,CAAC,KAAK,WAAW;GAChB,IAAI,UAAU,OAAO,MAAM,KAAK,GAAG;IACjC,OAAO,OAAO;IACd,IAAI,MACD,CAAC,KAAK,4BAAY,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK;GAEhD;EACF,CACF;EAEF,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"searchMiddleware.js","names":[],"sources":["../../src/searchMiddleware.ts"],"sourcesContent":["import { deepEqual, hasOwn } from './utils'\nimport type { NoInfer, PickOptional } from './utils'\nimport type {\n SearchMiddleware,\n SearchMiddlewareContext,\n SearchMiddlewareMeta,\n} from './route'\nimport type { IsRequiredParams } from './link'\n\ntype SearchMiddlewareNextWithMeta<TSearchSchema> = (\n newSearch: TSearchSchema,\n collectMeta: true,\n) => { search: TSearchSchema; meta: SearchMiddlewareMeta }\n\n/**\n * Retain specified search params across navigations.\n *\n * If `keys` is `true`, retain all current params. Otherwise, copy only the\n * listed keys from the current search into the next search.\n *\n * @param keys `true` to retain all, or a list of keys to retain.\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/retainSearchParamsFunction\n */\nexport function retainSearchParams<TSearchSchema extends object>(\n keys: Array<keyof TSearchSchema> | true,\n): SearchMiddleware<TSearchSchema> {\n return ({ search, next }) => {\n const { search: resultSearch, meta } = (\n next as unknown as SearchMiddlewareNextWithMeta<TSearchSchema>\n )(search, true)\n\n if (keys === true) {\n const copy = { ...search, ...resultSearch }\n const removed = meta.removed\n const explicit = meta.explicit\n for (const key of removed?.keys() || []) {\n if (\n (explicit && hasOwn.call(explicit, key)) ||\n deepEqual(search[key as keyof TSearchSchema], removed!.get(key))\n ) {\n delete copy[key as keyof TSearchSchema]\n }\n }\n for (const key of meta.removedAny || []) {\n delete copy[key as keyof TSearchSchema]\n }\n for (const key of meta.defaulted?.keys() || []) {\n if (\n key in search &&\n !meta.removedAny?.has(key) &&\n !(\n meta.removed?.has(key) &&\n ((explicit && hasOwn.call(explicit, key)) ||\n deepEqual(\n search[key as keyof TSearchSchema],\n meta.removed.get(key),\n ))\n )\n ) {\n copy[key as keyof TSearchSchema] = search[key as keyof TSearchSchema]\n }\n }\n return copy\n }\n\n const copy = { ...resultSearch }\n const explicit = meta.explicit\n // add missing keys from search to copy\n for (const key of keys) {\n const stringKey = key as string\n const removed =\n meta.removedAny?.has(stringKey) ||\n (meta.removed?.has(stringKey) &&\n ((explicit && hasOwn.call(explicit, stringKey)) ||\n deepEqual(search[key], meta.removed.get(stringKey))))\n if (\n !removed &&\n (!(key in copy) ||\n (key in search &&\n meta.defaulted?.has(stringKey) &&\n deepEqual(copy[key], meta.defaulted.get(stringKey))))\n ) {\n copy[key] = search[key]\n }\n }\n return copy\n }\n}\n\n/**\n * Remove optional or default-valued search params from navigations.\n *\n * - Pass `true` (only if there are no required search params) to strip all.\n * - Pass an array to always remove those optional keys.\n * - Pass an object of default values; keys equal (deeply) to the defaults are removed.\n *\n * @returns A search middleware suitable for route `search.middlewares`.\n * @link https://tanstack.com/router/latest/docs/framework/react/api/router/stripSearchParamsFunction\n */\nexport function stripSearchParams<\n TSearchSchema,\n TOptionalProps = PickOptional<NoInfer<TSearchSchema>>,\n const TValues = Partial<NoInfer<TSearchSchema>> | Array<keyof TOptionalProps>,\n const TInput = IsRequiredParams<TSearchSchema> extends never\n ? TValues | true\n : TValues,\n>(input: NoInfer<TInput>): SearchMiddleware<TSearchSchema> {\n return (({ search, next, meta }: SearchMiddlewareContext<TSearchSchema>) => {\n if (input === true) {\n Object.keys(search as object).forEach((key) => {\n if (meta) {\n ;(meta.removedAny ||= new Set()).add(key)\n }\n })\n return {}\n }\n const nextResult = next(search)\n const result = { ...nextResult } as Record<string, unknown>\n if (Array.isArray(input)) {\n input.forEach((key) => {\n delete result[key as string]\n if (meta) {\n ;(meta.removedAny ||= new Set()).add(key as string)\n }\n })\n } else {\n Object.entries(input as Record<string, unknown>).forEach(\n ([key, value]) => {\n if (deepEqual(result[key], value)) {\n delete result[key]\n if (meta) {\n ;(meta.removed ||= new Map()).set(key, value)\n }\n }\n },\n )\n }\n return result as any\n }) as SearchMiddleware<TSearchSchema>\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,SAAgB,mBACd,MACiC;CACjC,QAAQ,EAAE,QAAQ,WAAW;EAC3B,MAAM,EAAE,QAAQ,cAAc,SAC5B,KACA,QAAQ,IAAI;EAEd,IAAI,SAAS,MAAM;GACjB,MAAM,OAAO;IAAE,GAAG;IAAQ,GAAG;GAAa;GAC1C,MAAM,UAAU,KAAK;GACrB,MAAM,WAAW,KAAK;GACtB,KAAK,MAAM,OAAO,SAAS,KAAK,KAAK,CAAC,GACpC,IACG,YAAY,OAAO,KAAK,UAAU,GAAG,KACtC,UAAU,OAAO,MAA6B,QAAS,IAAI,GAAG,CAAC,GAE/D,OAAO,KAAK;GAGhB,KAAK,MAAM,OAAO,KAAK,cAAc,CAAC,GACpC,OAAO,KAAK;GAEd,KAAK,MAAM,OAAO,KAAK,WAAW,KAAK,KAAK,CAAC,GAC3C,IACE,OAAO,UACP,CAAC,KAAK,YAAY,IAAI,GAAG,KACzB,EACE,KAAK,SAAS,IAAI,GAAG,MACnB,YAAY,OAAO,KAAK,UAAU,GAAG,KACrC,UACE,OAAO,MACP,KAAK,QAAQ,IAAI,GAAG,CACtB,KAGJ,KAAK,OAA8B,OAAO;GAG9C,OAAO;EACT;EAEA,MAAM,OAAO,EAAE,GAAG,aAAa;EAC/B,MAAM,WAAW,KAAK;EAEtB,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,YAAY;GAMlB,IACE,EALA,KAAK,YAAY,IAAI,SAAS,KAC7B,KAAK,SAAS,IAAI,SAAS,MACxB,YAAY,OAAO,KAAK,UAAU,SAAS,KAC3C,UAAU,OAAO,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAC,QAGrD,EAAE,OAAO,SACP,OAAO,UACN,KAAK,WAAW,IAAI,SAAS,KAC7B,UAAU,KAAK,MAAM,KAAK,UAAU,IAAI,SAAS,CAAC,IAEtD,KAAK,OAAO,OAAO;EAEvB;EACA,OAAO;CACT;AACF;;;;;;;;;;;AAYA,SAAgB,kBAOd,OAAyD;CACzD,SAAS,EAAE,QAAQ,MAAM,WAAmD;EAC1E,IAAI,UAAU,MAAM;GAClB,OAAO,KAAK,MAAgB,EAAE,SAAS,QAAQ;IAC7C,IAAI,MACD,CAAC,KAAK,+BAAe,IAAI,IAAI,GAAG,IAAI,GAAG;GAE5C,CAAC;GACD,OAAO,CAAC;EACV;EAEA,MAAM,SAAS,EAAE,GADE,KAAK,MACJ,EAAW;EAC/B,IAAI,MAAM,QAAQ,KAAK,GACrB,MAAM,SAAS,QAAQ;GACrB,OAAO,OAAO;GACd,IAAI,MACD,CAAC,KAAK,+BAAe,IAAI,IAAI,GAAG,IAAI,GAAa;EAEtD,CAAC;OAED,OAAO,QAAQ,KAAgC,EAAE,SAC9C,CAAC,KAAK,WAAW;GAChB,IAAI,UAAU,OAAO,MAAM,KAAK,GAAG;IACjC,OAAO,OAAO;IACd,IAAI,MACD,CAAC,KAAK,4BAAY,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK;GAEhD;EACF,CACF;EAEF,OAAO;CACT;AACF"}
@@ -63,6 +63,7 @@ export declare function last<T>(arr: ReadonlyArray<T>): T | undefined;
63
63
  * Accepts either a literal value or a function of the previous value.
64
64
  */
65
65
  export declare function functionalUpdate<TPrevious, TResult = TPrevious>(updater: Updater<TPrevious, TResult> | NonNullableUpdater<TPrevious, TResult>, previous: TPrevious): TResult;
66
+ export declare const hasOwn: (v: PropertyKey) => boolean;
66
67
  export declare function hasKeys(obj: Record<string, unknown>): boolean;
67
68
  export declare const createNull: () => any;
68
69
  export declare const nullReplaceEqualDeep: typeof replaceEqualDeep;
package/dist/esm/utils.js CHANGED
@@ -321,6 +321,6 @@ function arraysEqual(a, b) {
321
321
  return true;
322
322
  }
323
323
  //#endregion
324
- export { DEFAULT_PROTOCOL_ALLOWLIST, arraysEqual, buildDevStylesUrl, createControlledPromise, decodePath, deepEqual, encodePathLikeUrl, escapeHtml, findLast, functionalUpdate, hasKeys, isDangerousProtocol, isModuleNotFoundError, isPlainArray, isPlainObject, isPromise, last, nullReplaceEqualDeep, replaceEqualDeep };
324
+ export { DEFAULT_PROTOCOL_ALLOWLIST, arraysEqual, buildDevStylesUrl, createControlledPromise, decodePath, deepEqual, encodePathLikeUrl, escapeHtml, findLast, functionalUpdate, hasKeys, hasOwn, isDangerousProtocol, isModuleNotFoundError, isPlainArray, isPlainObject, isPromise, last, nullReplaceEqualDeep, replaceEqualDeep };
325
325
 
326
326
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"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\nexport function hasKeys(obj: Record<string, unknown>) {\n for (const key in obj) {\n if (hasOwn.call(obj, key)) return true\n }\n return false\n}\n\nexport const createNull = () => Object.create(null)\nexport const nullReplaceEqualDeep: typeof replaceEqualDeep = (prev, next) =>\n replaceEqualDeep(prev, next, createNull)\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>(\n prev: any,\n _next: T,\n _makeObj = () => ({}),\n _depth = 0,\n): 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) : _makeObj()\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, _makeObj, _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 * Default list of URL protocols to allow in links, redirects, and navigation.\n * Any absolute URL protocol not in this list is treated as dangerous by default.\n */\nexport const DEFAULT_PROTOCOL_ALLOWLIST = [\n // Standard web navigation\n 'http:',\n 'https:',\n\n // Common browser-safe actions\n 'mailto:',\n 'tel:',\n]\n\n/**\n * Check if a URL string uses a protocol that is not in the allowlist.\n * Returns true for blocked protocols like javascript:, blob:, data:, 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 * @param allowlist - Set of protocols to allow\n * @returns true if the URL uses a protocol that is not allowed\n */\nexport function isDangerousProtocol(\n url: string,\n allowlist: Set<string>,\n): 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 !allowlist.has(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\nexport function arraysEqual<T>(a: Array<T>, b: Array<T>) {\n if (a === b) return true\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n"],"mappings":";;;;;;AA+LA,SAAgB,KAAQ,KAAuB;CAC7C,OAAO,IAAI,IAAI,SAAS;AAC1B;AAEA,SAAS,WAAW,GAAuB;CACzC,OAAO,OAAO,MAAM;AACtB;;;;;AAMA,SAAgB,iBACd,SACA,UACS;CACT,IAAI,WAAW,OAAO,GACpB,OAAO,QAAQ,QAAQ;CAGzB,OAAO;AACT;AAEA,MAAM,SAAS,OAAO,UAAU;AAChC,MAAM,eAAe,OAAO,UAAU;AAEtC,SAAgB,QAAQ,KAA8B;CACpD,KAAK,MAAM,OAAO,KAChB,IAAI,OAAO,KAAK,KAAK,GAAG,GAAG,OAAO;CAEpC,OAAO;AACT;AAEA,MAAa,mBAAmB,OAAO,OAAO,IAAI;AAClD,MAAa,wBAAiD,MAAM,SAClE,iBAAiB,MAAM,MAAM,UAAU;;;;;;;AAQzC,SAAgB,iBACd,MACA,OACA,kBAAkB,CAAC,IACnB,SAAS,GACN;CACH,IAAI,UACF,OAAO;CAET,IAAI,SAAS,OACX,OAAO;CAGT,IAAI,SAAS,KAAK,OAAO;CAEzB,MAAM,OAAO;CAEb,MAAM,QAAQ,aAAa,IAAI,KAAK,aAAa,IAAI;CAErD,IAAI,CAAC,SAAS,EAAE,cAAc,IAAI,KAAK,cAAc,IAAI,IAAI,OAAO;CAEpE,MAAM,YAAY,QAAQ,OAAO,qBAAqB,IAAI;CAC1D,IAAI,CAAC,WAAW,OAAO;CACvB,MAAM,YAAY,QAAQ,OAAO,qBAAqB,IAAI;CAC1D,IAAI,CAAC,WAAW,OAAO;CACvB,MAAM,WAAW,UAAU;CAC3B,MAAM,WAAW,UAAU;CAC3B,MAAM,OAAY,QAAQ,IAAI,MAAM,QAAQ,IAAI,SAAS;CAEzD,IAAI,aAAa;CAEjB,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAAK;EACjC,MAAM,MAAM,QAAQ,IAAK,UAAU;EACnC,MAAM,IAAI,KAAK;EACf,MAAM,IAAI,KAAK;EAEf,IAAI,MAAM,GAAG;GACX,KAAK,OAAO;GACZ,IAAI,QAAQ,IAAI,WAAW,OAAO,KAAK,MAAM,GAAG,GAAG;GACnD;EACF;EAEA,IACE,MAAM,QACN,MAAM,QACN,OAAO,MAAM,YACb,OAAO,MAAM,UACb;GACA,KAAK,OAAO;GACZ;EACF;EAEA,MAAM,IAAI,iBAAiB,GAAG,GAAG,UAAU,SAAS,CAAC;EACrD,KAAK,OAAO;EACZ,IAAI,MAAM,GAAG;CACf;CAEA,OAAO,aAAa,YAAY,eAAe,WAAW,OAAO;AACnE;;;;;;;AAQA,SAAS,qBAAqB,GAAW;CACvC,MAAM,QAAQ,OAAO,oBAAoB,CAAC;CAG1C,KAAK,MAAM,QAAQ,OACjB,IAAI,CAAC,aAAa,KAAK,GAAG,IAAI,GAAG,OAAO;CAI1C,MAAM,UAAU,OAAO,sBAAsB,CAAC;CAG9C,IAAI,QAAQ,WAAW,GAAG,OAAO;CAGjC,MAAM,OAA+B;CACrC,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,CAAC,aAAa,KAAK,GAAG,MAAM,GAAG,OAAO;EAC1C,KAAK,KAAK,MAAM;CAClB;CACA,OAAO;AACT;AAGA,SAAgB,cAAc,GAAQ;CACpC,IAAI,CAAC,mBAAmB,CAAC,GACvB,OAAO;CAIT,MAAM,OAAO,EAAE;CACf,IAAI,OAAO,SAAS,aAClB,OAAO;CAIT,MAAM,OAAO,KAAK;CAClB,IAAI,CAAC,mBAAmB,IAAI,GAC1B,OAAO;CAIT,IAAI,CAAC,KAAK,eAAe,eAAe,GACtC,OAAO;CAIT,OAAO;AACT;AAEA,SAAS,mBAAmB,GAAQ;CAClC,OAAO,OAAO,UAAU,SAAS,KAAK,CAAC,MAAM;AAC/C;;;;AAKA,SAAgB,aAAa,OAAyC;CACpE,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,OAAO,KAAK,KAAK,EAAE;AACrE;;;;;AAMA,SAAgB,UACd,GACA,GACA,MACS;CACT,IAAI,MAAM,GACR,OAAO;CAGT,IAAI,OAAO,MAAM,OAAO,GACtB,OAAO;CAGT,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;EACxC,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;EAClC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAI,GAAG,KACnC,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,OAAO;EAE3C,OAAO;CACT;CAEA,IAAI,cAAc,CAAC,KAAK,cAAc,CAAC,GAAG;EACxC,MAAM,kBAAkB,MAAM,mBAAmB;EAEjD,IAAI,MAAM,SAAS;GACjB,KAAK,MAAM,KAAK,GACd,IAAI,CAAC,mBAAmB,EAAE,OAAO,KAAA;QAC3B,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,OAAO;GAAA;GAG7C,OAAO;EACT;EAEA,IAAI,SAAS;EACb,IAAI,CAAC,iBACH,SAAS,OAAO,KAAK,CAAC,EAAE;OAExB,KAAK,MAAM,KAAK,GACd,IAAI,EAAE,OAAO,KAAA,GAAW;EAI5B,IAAI,SAAS;EACb,KAAK,MAAM,KAAK,GACd,IAAI,CAAC,mBAAmB,EAAE,OAAO,KAAA,GAAW;GAC1C;GACA,IAAI,SAAS,UAAU,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,OAAO;EAC9D;EAGF,OAAO,WAAW;CACpB;CAEA,OAAO;AACT;;;;;AA0CA,SAAgB,wBAA2B,WAAgC;CACzE,IAAI;CACJ,IAAI;CAEJ,MAAM,oBAAoB,IAAI,SAAY,SAAS,WAAW;EAC5D,qBAAqB;EACrB,oBAAoB;CACtB,CAAC;CAED,kBAAkB,SAAS;CAE3B,kBAAkB,WAAW,UAAa;EACxC,kBAAkB,SAAS;EAC3B,kBAAkB,QAAQ;EAC1B,mBAAmB,KAAK;EACxB,YAAY,KAAK;CACnB;CAEA,kBAAkB,UAAU,MAAM;EAChC,kBAAkB,SAAS;EAC3B,kBAAkB,CAAC;CACrB;CAEA,OAAO;AACT;;;;;AAMA,SAAgB,sBAAsB,OAAqB;CAIzD,IAAI,OAAO,OAAO,YAAY,UAAU,OAAO;CAC/C,OACE,MAAM,QAAQ,WAAW,6CAA6C,KACtE,MAAM,QAAQ,WAAW,2CAA2C,KACpE,MAAM,QAAQ,WAAW,kCAAkC;AAE/D;AAEA,SAAgB,UACd,OAC8B;CAC9B,OAAO,QACL,SACA,OAAO,UAAU,YACjB,OAAQ,MAAqB,SAAS,UACxC;AACF;AAEA,SAAgB,SACd,OACA,WACe;CACf,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK;EAC1C,MAAM,OAAO,MAAM;EACnB,IAAI,UAAU,IAAI,GAAG,OAAO;CAC9B;AAEF;;;;;;AAOA,SAAS,oBAAoB,SAAyB;CAIpD,OAAO,QAAQ,QAAQ,oBAAoB,EAAE;AAC/C;AAEA,SAAS,cAAc,SAAyB;CAC9C,IAAI;CACJ,IAAI;EACF,UAAU,UAAU,OAAO;CAC7B,QAAQ;EAEN,UAAU,QAAQ,WAAW,mBAAmB,UAAU;GACxD,IAAI;IACF,OAAO,UAAU,KAAK;GACxB,QAAQ;IACN,OAAO;GACT;EACF,CAAC;CACH;CACA,OAAO,oBAAoB,OAAO;AACpC;;;;;AAMA,MAAa,6BAA6B;CAExC;CACA;CAGA;CACA;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,oBACd,KACA,WACS;CACT,IAAI,CAAC,KAAK,OAAO;CAEjB,IAAI;EAGF,MAAM,SAAS,IAAI,IAAI,GAAG;EAC1B,OAAO,CAAC,UAAU,IAAI,OAAO,QAAQ;CACvC,QAAQ;EAGN,OAAO;CACT;AACF;AAIA,MAAM,qBAAkD;CACtD,KAAK;CACL,KAAK;CACL,KAAK;CACL,UAAU;CACV,UAAU;AACZ;AAEA,MAAM,oBAAoB;;;;;;;;AAS1B,SAAgB,WAAW,KAAqB;CAC9C,OAAO,IAAI,QAAQ,oBAAoB,UAAU,mBAAmB,MAAO;AAC7E;AAEA,SAAgB,WAAW,MAAc;CACvC,IAAI,CAAC,MAAM,OAAO;EAAE;EAAM,4BAA4B;CAAM;CAO5D,IAAI,CAAC,qBAAqB,KAAK,IAAI,KAAK,CAAC,KAAK,WAAW,IAAI,GAC3D,OAAO;EAAE;EAAM,4BAA4B;CAAM;CAGnD,MAAM,KAAK;CACX,IAAI,SAAS;CACb,IAAI,SAAS;CACb,IAAI;CACJ,OAAO,UAAU,QAAQ,GAAG,KAAK,IAAI,IAAI;EACvC,UAAU,cAAc,KAAK,MAAM,QAAQ,MAAM,KAAK,CAAC,IAAI,MAAM;EACjE,SAAS,GAAG;CACd;CACA,SAAS,SAAS,cAAc,SAAS,KAAK,MAAM,MAAM,IAAI,IAAI;CAKlE,IAAI,6BAA6B;CACjC,IAAI,OAAO,WAAW,IAAI,GAAG;EAC3B,6BAA6B;EAC7B,SAAS,MAAM,OAAO,QAAQ,QAAQ,EAAE;CAC1C;CAEA,OAAO;EAAE,MAAM;EAAQ;CAA2B;AACpD;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,kBAAkB,MAAsB;CAKtD,IAAI,CAAC,sBAAsB,KAAK,IAAI,GAAG,OAAO;CAG9C,OAAO,KAAK,QAAQ,yBAAyB,kBAAkB;AACjE;;;;;;;;;;AAWA,SAAgB,kBACd,UACA,UACQ;CAER,MAAM,kBAAkB,SAAS,QAAQ,cAAc,EAAE;CAGzD,OAAO,GADoB,oBAAoB,KAAK,KAAK,IAAI,kBAChC,qCAAqC,mBAAmB,SAAS,KAAK,GAAG,CAAC;AACzG;AAEA,SAAgB,YAAe,GAAa,GAAa;CACvD,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAC5B,IAAI,EAAE,OAAO,EAAE,IAAI,OAAO;CAE5B,OAAO;AACT"}
1
+ {"version":3,"file":"utils.js","names":[],"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\nexport const hasOwn = Object.prototype.hasOwnProperty\nconst isEnumerable = Object.prototype.propertyIsEnumerable\n\nexport function hasKeys(obj: Record<string, unknown>) {\n for (const key in obj) {\n if (hasOwn.call(obj, key)) return true\n }\n return false\n}\n\nexport const createNull = () => Object.create(null)\nexport const nullReplaceEqualDeep: typeof replaceEqualDeep = (prev, next) =>\n replaceEqualDeep(prev, next, createNull)\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>(\n prev: any,\n _next: T,\n _makeObj = () => ({}),\n _depth = 0,\n): 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) : _makeObj()\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, _makeObj, _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 * Default list of URL protocols to allow in links, redirects, and navigation.\n * Any absolute URL protocol not in this list is treated as dangerous by default.\n */\nexport const DEFAULT_PROTOCOL_ALLOWLIST = [\n // Standard web navigation\n 'http:',\n 'https:',\n\n // Common browser-safe actions\n 'mailto:',\n 'tel:',\n]\n\n/**\n * Check if a URL string uses a protocol that is not in the allowlist.\n * Returns true for blocked protocols like javascript:, blob:, data:, 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 * @param allowlist - Set of protocols to allow\n * @returns true if the URL uses a protocol that is not allowed\n */\nexport function isDangerousProtocol(\n url: string,\n allowlist: Set<string>,\n): 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 !allowlist.has(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\nexport function arraysEqual<T>(a: Array<T>, b: Array<T>) {\n if (a === b) return true\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n return true\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.171.11",
3
+ "version": "1.171.13",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
package/src/route.ts CHANGED
@@ -78,6 +78,7 @@ export type SearchMiddlewareMeta = {
78
78
  removed?: Map<string, unknown>
79
79
  removedAny?: Set<string>
80
80
  defaulted?: Map<string, unknown>
81
+ explicit?: unknown
81
82
  }
82
83
 
83
84
  export type SearchMiddlewareContext<TSearchSchema> = {
package/src/router.ts CHANGED
@@ -964,13 +964,16 @@ export class RouterCore<
964
964
  tempLocationKey: string | undefined = `${Math.round(
965
965
  Math.random() * 10000000,
966
966
  )}`
967
- resetNextScroll = true
967
+ _scroll: {
968
+ next: boolean
969
+ restoring?: boolean
970
+ restoration?: boolean
971
+ reset?: boolean
972
+ } = { next: true }
968
973
  shouldViewTransition?: boolean | ViewTransitionOptions = undefined
969
974
  isViewTransitionTypesSupported?: boolean = undefined
970
975
  subscribers = new Set<RouterListener<RouterEvent>>()
971
976
  viewTransitionPromise?: ControlledPromise<true>
972
- isScrollRestoring = false
973
- isScrollRestorationSetup = false
974
977
 
975
978
  // Must build in constructor
976
979
  stores!: RouterStores<TRouteTree>
@@ -2227,7 +2230,7 @@ export class RouterCore<
2227
2230
  )
2228
2231
  }
2229
2232
 
2230
- this.resetNextScroll = next.resetScroll ?? true
2233
+ this._scroll.next = next.resetScroll ?? true
2231
2234
 
2232
2235
  if (!this.history.subscribers.size) {
2233
2236
  this.load(
@@ -3194,7 +3197,11 @@ function buildMiddlewareChain(destRoutes: ReadonlyArray<AnyRoute>) {
3194
3197
  if (dest.search === true) {
3195
3198
  return currentSearch
3196
3199
  }
3197
- return functionalUpdate(dest.search, currentSearch)
3200
+ const result = functionalUpdate(dest.search, currentSearch)
3201
+ if (meta) {
3202
+ meta.explicit = result
3203
+ }
3204
+ return result
3198
3205
  }
3199
3206
 
3200
3207
  const next = (newSearch: any, collectMeta?: true): any => {
@@ -167,18 +167,17 @@ function getScrollToTopElements(
167
167
 
168
168
  export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
169
169
  // Keep hash/top scrolling active even when sessionStorage is unavailable.
170
+ const shouldSetupScrollRestoration = force ?? router.options.scrollRestoration
171
+ const scroll = router._scroll
170
172
 
171
- if (force ?? router.options.scrollRestoration) {
172
- router.isScrollRestoring = true
173
+ if (shouldSetupScrollRestoration) {
174
+ scroll.restoring = true
173
175
  }
174
176
 
175
- if ((isServer ?? router.isServer) || router.isScrollRestorationSetup) {
177
+ if (isServer ?? router.isServer) {
176
178
  return
177
179
  }
178
180
 
179
- router.isScrollRestorationSetup = true
180
- ignoreScroll = false
181
-
182
181
  const getKey =
183
182
  router.options.getScrollRestorationKey || defaultGetScrollRestorationKey
184
183
  const trackedScrollEntries = new Map<ScrollTarget, ScrollRestorationEntry>()
@@ -194,10 +193,8 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
194
193
  trackedScrollEntries.set(target, entry)
195
194
  }
196
195
 
197
- history.scrollRestoration = 'manual'
198
-
199
196
  const onScroll = (event: Event) => {
200
- if (ignoreScroll || !router.isScrollRestoring) {
197
+ if (ignoreScroll || !scroll.restoring) {
201
198
  return
202
199
  }
203
200
 
@@ -211,7 +208,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
211
208
 
212
209
  // Snapshot the current page's tracked scroll targets before navigation or unload.
213
210
  const snapshotCurrentScrollTargets = (restoreKey: string) => {
214
- if (!router.isScrollRestoring) {
211
+ if (!scroll.restoring) {
215
212
  return
216
213
  }
217
214
 
@@ -227,32 +224,45 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
227
224
  }
228
225
  }
229
226
 
230
- document.addEventListener('scroll', onScroll, true)
231
- router.subscribe('onBeforeLoad', (event) => {
232
- if (event.fromLocation) {
233
- snapshotCurrentScrollTargets(getKey(event.fromLocation))
234
- }
235
- trackedScrollEntries.clear()
236
- })
237
- addEventListener('pagehide', () => {
238
- snapshotCurrentScrollTargets(
239
- getKey(
240
- router.stores.resolvedLocation.get() ?? router.stores.location.get(),
241
- ),
242
- )
243
- persistScrollRestorationCache()
244
- })
227
+ if (shouldSetupScrollRestoration && !scroll.restoration) {
228
+ scroll.restoration = true
229
+ ignoreScroll = false
230
+
231
+ history.scrollRestoration = 'manual'
232
+
233
+ document.addEventListener('scroll', onScroll, true)
234
+ router.subscribe('onBeforeLoad', (event) => {
235
+ if (event.fromLocation) {
236
+ snapshotCurrentScrollTargets(getKey(event.fromLocation))
237
+ }
238
+ trackedScrollEntries.clear()
239
+ })
240
+ addEventListener('pagehide', () => {
241
+ snapshotCurrentScrollTargets(
242
+ getKey(
243
+ router.stores.resolvedLocation.get() ?? router.stores.location.get(),
244
+ ),
245
+ )
246
+ persistScrollRestorationCache()
247
+ })
248
+ }
249
+
250
+ if (scroll.reset) {
251
+ return
252
+ }
253
+
254
+ scroll.reset = true
245
255
 
246
256
  // Restore destination scroll after the new route has rendered.
247
257
  router.subscribe('onRendered', (event) => {
248
258
  const behavior = router.options.scrollRestorationBehavior
249
259
  const scrollToTopSelectors = router.options.scrollToTopSelectors
250
- const shouldResetScroll = router.resetNextScroll
260
+ const shouldResetScroll = scroll.next
251
261
  let scrollToTopElements: Array<Element> | undefined
252
262
  trackedScrollEntries.clear()
253
263
 
254
264
  if (!shouldResetScroll) {
255
- router.resetNextScroll = true
265
+ scroll.next = true
256
266
  }
257
267
 
258
268
  if (
@@ -265,7 +275,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
265
275
  const cacheKey = getKey(event.toLocation)
266
276
  const fromCacheKey = event.fromLocation && getKey(event.fromLocation)
267
277
 
268
- if (router.isScrollRestoring && fromCacheKey && fromCacheKey !== cacheKey) {
278
+ if (scroll.restoring && fromCacheKey && fromCacheKey !== cacheKey) {
269
279
  const fromElementEntries = scrollRestorationCache[fromCacheKey]
270
280
 
271
281
  if (fromElementEntries) {
@@ -317,7 +327,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
317
327
  hashScrollIntoViewOptions &&
318
328
  (action === 'PUSH' || action === 'REPLACE')
319
329
 
320
- const elementEntries = router.isScrollRestoring
330
+ const elementEntries = scroll.restoring
321
331
  ? scrollRestorationCache[cacheKey]
322
332
  : undefined
323
333
 
@@ -1,4 +1,4 @@
1
- import { deepEqual } from './utils'
1
+ import { deepEqual, hasOwn } from './utils'
2
2
  import type { NoInfer, PickOptional } from './utils'
3
3
  import type {
4
4
  SearchMiddleware,
@@ -33,8 +33,12 @@ export function retainSearchParams<TSearchSchema extends object>(
33
33
  if (keys === true) {
34
34
  const copy = { ...search, ...resultSearch }
35
35
  const removed = meta.removed
36
+ const explicit = meta.explicit
36
37
  for (const key of removed?.keys() || []) {
37
- if (deepEqual(search[key as keyof TSearchSchema], removed!.get(key))) {
38
+ if (
39
+ (explicit && hasOwn.call(explicit, key)) ||
40
+ deepEqual(search[key as keyof TSearchSchema], removed!.get(key))
41
+ ) {
38
42
  delete copy[key as keyof TSearchSchema]
39
43
  }
40
44
  }
@@ -47,7 +51,11 @@ export function retainSearchParams<TSearchSchema extends object>(
47
51
  !meta.removedAny?.has(key) &&
48
52
  !(
49
53
  meta.removed?.has(key) &&
50
- deepEqual(search[key as keyof TSearchSchema], meta.removed.get(key))
54
+ ((explicit && hasOwn.call(explicit, key)) ||
55
+ deepEqual(
56
+ search[key as keyof TSearchSchema],
57
+ meta.removed.get(key),
58
+ ))
51
59
  )
52
60
  ) {
53
61
  copy[key as keyof TSearchSchema] = search[key as keyof TSearchSchema]
@@ -57,13 +65,15 @@ export function retainSearchParams<TSearchSchema extends object>(
57
65
  }
58
66
 
59
67
  const copy = { ...resultSearch }
68
+ const explicit = meta.explicit
60
69
  // add missing keys from search to copy
61
70
  for (const key of keys) {
62
71
  const stringKey = key as string
63
72
  const removed =
64
73
  meta.removedAny?.has(stringKey) ||
65
74
  (meta.removed?.has(stringKey) &&
66
- deepEqual(search[key], meta.removed.get(stringKey)))
75
+ ((explicit && hasOwn.call(explicit, stringKey)) ||
76
+ deepEqual(search[key], meta.removed.get(stringKey))))
67
77
  if (
68
78
  !removed &&
69
79
  (!(key in copy) ||
package/src/utils.ts CHANGED
@@ -212,7 +212,7 @@ export function functionalUpdate<TPrevious, TResult = TPrevious>(
212
212
  return updater
213
213
  }
214
214
 
215
- const hasOwn = Object.prototype.hasOwnProperty
215
+ export const hasOwn = Object.prototype.hasOwnProperty
216
216
  const isEnumerable = Object.prototype.propertyIsEnumerable
217
217
 
218
218
  export function hasKeys(obj: Record<string, unknown>) {