@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.
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +1 -0
- package/dist/cjs/router.cjs +5 -5
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +6 -3
- package/dist/cjs/scroll-restoration.cjs +26 -20
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/cjs/searchMiddleware.cjs +5 -3
- package/dist/cjs/searchMiddleware.cjs.map +1 -1
- package/dist/cjs/utils.cjs +1 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +1 -0
- package/dist/esm/route.d.ts +1 -0
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +6 -3
- package/dist/esm/router.js +5 -5
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.js +26 -20
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/searchMiddleware.js +6 -4
- package/dist/esm/searchMiddleware.js.map +1 -1
- package/dist/esm/utils.d.ts +1 -0
- package/dist/esm/utils.js +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/route.ts +1 -0
- package/src/router.ts +12 -5
- package/src/scroll-restoration.ts +39 -29
- package/src/searchMiddleware.ts +14 -4
- package/src/utils.ts +1 -1
|
@@ -76,10 +76,10 @@ function getScrollToTopElements(scrollToTopSelectors) {
|
|
|
76
76
|
return elements;
|
|
77
77
|
}
|
|
78
78
|
function setupScrollRestoration(router, force) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 || !
|
|
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 (!
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 =
|
|
124
|
+
const shouldResetScroll = scroll.next;
|
|
119
125
|
let scrollToTopElements;
|
|
120
126
|
trackedScrollEntries.clear();
|
|
121
|
-
if (!shouldResetScroll)
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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]
|
|
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"}
|
package/dist/esm/utils.d.ts
CHANGED
|
@@ -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
|
package/dist/esm/utils.js.map
CHANGED
|
@@ -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":"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"}
|
|
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
package/src/route.ts
CHANGED
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
172
|
-
|
|
173
|
+
if (shouldSetupScrollRestoration) {
|
|
174
|
+
scroll.restoring = true
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
if (
|
|
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 || !
|
|
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 (!
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
)
|
|
243
|
-
|
|
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 =
|
|
260
|
+
const shouldResetScroll = scroll.next
|
|
251
261
|
let scrollToTopElements: Array<Element> | undefined
|
|
252
262
|
trackedScrollEntries.clear()
|
|
253
263
|
|
|
254
264
|
if (!shouldResetScroll) {
|
|
255
|
-
|
|
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 (
|
|
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 =
|
|
330
|
+
const elementEntries = scroll.restoring
|
|
321
331
|
? scrollRestorationCache[cacheKey]
|
|
322
332
|
: undefined
|
|
323
333
|
|
package/src/searchMiddleware.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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>) {
|