mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
fix(studio): guard unsaved edits in TanStack
This commit is contained in:
1 parent
07c75e84fc
commit
bb082e5429
8 files changed
+411
-13
No files matched your search
@@ -469,8 +469,9 @@ inline.
|
||||
`beforeHistoryChange` / `hashChangeStart` / `hashChangeComplete`.
|
||||
**Known gap:** Next's throw-from-`routeChangeStart`-to-cancel pattern
|
||||
isn't supportable — `subscribe` is fire-and-forget.
|
||||
`usePreventNavigationOnUnsavedChanges` relies on it and needs
|
||||
migrating to TanStack's `useBlocker` separately.
|
||||
`usePreventNavigationOnUnsavedChanges` uses a Vite alias to
|
||||
`compat/usePreventNavigationOnUnsavedChanges.ts`, which blocks through
|
||||
TanStack's native `useBlocker`. Next keeps the original hook.
|
||||
- `api.ts` — `toWebHandler(nextHandler)`. See **API routes → Shim
|
||||
coverage** above.
|
||||
- `link.tsx`, `navigation.ts`, `dynamic.tsx`, `image.tsx`,
|
||||
@@ -616,7 +617,7 @@ for the Vite pipeline:
|
||||
|
||||
- Switch `routes/index.tsx` redirects from `href` to `to` — all targets
|
||||
now live in the TanStack tree.
|
||||
- Migrate `usePreventNavigationOnUnsavedChanges` from `router.events.on('routeChangeStart', …)` (throw-to-cancel pattern) to TanStack's `useBlocker`.
|
||||
- Remove the Vite alias for `usePreventNavigationOnUnsavedChanges` and replace the original Next hook with the native implementation in `compat/usePreventNavigationOnUnsavedChanges.ts`.
|
||||
- Drop the `_splat` / `routeSlug` normalisation block from
|
||||
`pages/org/_/[[...routeSlug]].tsx` + `pages/project/_/[[...routeSlug]].tsx` (only there to keep both runtimes mounting the same body).
|
||||
- Remove `RouteValidationWrapper` + `next/router` compat shim usage from `__root.tsx`.
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
//
|
||||
// Known gap: Next's `routeChangeStart` lets handlers throw to cancel the
|
||||
// navigation. TanStack's `subscribe` is fire-and-forget; cancellation
|
||||
// requires `useBlocker` instead. `usePreventNavigationOnUnsavedChanges`
|
||||
// relies on the throw-to-cancel pattern and will need migrating to
|
||||
// `useBlocker` separately.
|
||||
// requires `useBlocker` instead. Vite selects a native implementation of
|
||||
// `usePreventNavigationOnUnsavedChanges` so that guard bypasses this adapter.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyRouter = any
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import {
|
||||
createBrowserHistory,
|
||||
createRootRoute,
|
||||
createRoute,
|
||||
createRouter,
|
||||
RouterContextProvider,
|
||||
type AnyRouter,
|
||||
} from '@tanstack/react-router'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { usePreventNavigationOnUnsavedChanges } from './usePreventNavigationOnUnsavedChanges'
|
||||
import { parseSearch, stringifySearch } from '@/lib/router-search-params'
|
||||
|
||||
let destroyHistory: (() => void) | undefined
|
||||
|
||||
function renderNavigationGuard(hasChanges = true, basepath = '/') {
|
||||
const editorPath = `${basepath === '/' ? '' : basepath}/editor`
|
||||
window.history.replaceState(null, '', editorPath)
|
||||
const history = createBrowserHistory()
|
||||
destroyHistory = () => history.destroy()
|
||||
const rootRoute = createRootRoute()
|
||||
const routeTree = rootRoute.addChildren([
|
||||
createRoute({ getParentRoute: () => rootRoute, path: '/editor' }),
|
||||
createRoute({ getParentRoute: () => rootRoute, path: '/overview' }),
|
||||
createRoute({ getParentRoute: () => rootRoute, path: '/other' }),
|
||||
])
|
||||
const router = createRouter({ routeTree, history, basepath, parseSearch, stringifySearch })
|
||||
const hook = renderHook(
|
||||
({ hasChanges }) => usePreventNavigationOnUnsavedChanges({ hasChanges }),
|
||||
{
|
||||
initialProps: { hasChanges },
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<RouterContextProvider router={router}>{children}</RouterContextProvider>
|
||||
),
|
||||
}
|
||||
)
|
||||
return { ...hook, router, history, editorPath }
|
||||
}
|
||||
|
||||
function dispatchBeforeUnload() {
|
||||
const event = new Event('beforeunload', { cancelable: true })
|
||||
window.dispatchEvent(event)
|
||||
return event
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
destroyHistory?.()
|
||||
destroyHistory = undefined
|
||||
})
|
||||
|
||||
describe('TanStack unsaved-change navigation guard', () => {
|
||||
it('allows clean navigation without confirmation', async () => {
|
||||
const { result, router, history } = renderNavigationGuard(false)
|
||||
|
||||
await act(async () => {
|
||||
await router.navigate<AnyRouter, string>({ to: '/overview' })
|
||||
})
|
||||
|
||||
expect(history.location.pathname).toBe('/overview')
|
||||
expect(window.location.pathname).toBe('/overview')
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the URL and history unchanged until a dirty navigation is confirmed', async () => {
|
||||
const { result, router, history } = renderNavigationGuard()
|
||||
const initialLength = history.length
|
||||
|
||||
act(() => {
|
||||
void router.navigate<AnyRouter, string>({ to: '/overview' })
|
||||
})
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
|
||||
expect(history.location.pathname).toBe('/editor')
|
||||
expect(window.location.pathname).toBe('/editor')
|
||||
expect(history.length).toBe(initialLength)
|
||||
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
expect(history.location.pathname).toBe('/overview')
|
||||
expect(history.length).toBe(initialLength + 1)
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
|
||||
it('cancels dirty navigation and can cancel another destination afterward', async () => {
|
||||
const { result, history } = renderNavigationGuard()
|
||||
const initialLength = history.length
|
||||
|
||||
for (const target of ['/overview', '/other']) {
|
||||
act(() => history.push(target))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(false))
|
||||
|
||||
expect(history.location.pathname).toBe('/editor')
|
||||
expect(window.location.pathname).toBe('/editor')
|
||||
expect(history.length).toBe(initialLength)
|
||||
}
|
||||
})
|
||||
|
||||
it('confirms the new destination on the first click after canceling a previous attempt', async () => {
|
||||
const { result, history } = renderNavigationGuard()
|
||||
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(false))
|
||||
|
||||
act(() => history.push('/other?tab=details#section'))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
|
||||
await waitFor(() => expect(history.location.href).toBe('/other?tab=details#section'))
|
||||
expect(`${window.location.pathname}${window.location.search}${window.location.hash}`).toBe(
|
||||
'/other?tab=details#section'
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves replace navigation and base paths when confirming', async () => {
|
||||
const { result, router, history } = renderNavigationGuard(true, '/dashboard')
|
||||
const initialLength = history.length
|
||||
|
||||
act(() => {
|
||||
void router.navigate<AnyRouter, string>({ to: '/overview', replace: true })
|
||||
})
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
expect(window.location.pathname).toBe('/dashboard/editor')
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/dashboard/overview'))
|
||||
expect(history.length).toBe(initialLength)
|
||||
})
|
||||
|
||||
it('preserves encoded query values, array values, and fragments through browser history', async () => {
|
||||
const { result, router, history } = renderNavigationGuard()
|
||||
const search = {
|
||||
s: 'select timestamp\nfrom logs\nlimit 5',
|
||||
filter: ['name:eq:日本語', 'count:gt:2'],
|
||||
q: 'space + plus % percent',
|
||||
}
|
||||
|
||||
act(() => {
|
||||
void router.navigate<AnyRouter, string>({ to: '/overview', search, hash: 'details' })
|
||||
})
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
expect(window.location.pathname).toBe('/editor')
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
expect(history.location.href).toBe(`/overview${stringifySearch(search)}#details`)
|
||||
expect(window.location.search).toContain('%0A')
|
||||
expect(parseSearch(window.location.search)).toEqual(search)
|
||||
expect(window.location.hash).toBe('#details')
|
||||
})
|
||||
|
||||
it('guards the next navigation after confirming one while the editor remains mounted', async () => {
|
||||
const { result, history } = renderNavigationGuard()
|
||||
|
||||
act(() => history.push('/editor?tab=second'))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
await waitFor(() => expect(history.location.href).toBe('/editor?tab=second'))
|
||||
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(false))
|
||||
expect(history.location.href).toBe('/editor?tab=second')
|
||||
})
|
||||
|
||||
it('uses the latest dirty state and allows navigation after saving', async () => {
|
||||
const { result, rerender, history } = renderNavigationGuard(false)
|
||||
rerender({ hasChanges: true })
|
||||
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(false))
|
||||
rerender({ hasChanges: false })
|
||||
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
|
||||
it('cancels browser Back and subsequently confirms it without adding a history entry', async () => {
|
||||
const { result, rerender, history } = renderNavigationGuard(false)
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
const initialLength = history.length
|
||||
rerender({ hasChanges: true })
|
||||
|
||||
act(() => window.history.back())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
expect(history.location.pathname).toBe('/overview')
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
expect(history.location.pathname).toBe('/overview')
|
||||
|
||||
act(() => window.history.back())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
await waitFor(() => expect(history.location.pathname).toBe('/editor'))
|
||||
expect(window.location.pathname).toBe('/editor')
|
||||
expect(history.length).toBe(initialLength)
|
||||
})
|
||||
|
||||
it('confirms browser Forward without adding a history entry', async () => {
|
||||
const { result, rerender, history } = renderNavigationGuard(false)
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
act(() => window.history.back())
|
||||
await waitFor(() => expect(history.location.pathname).toBe('/editor'))
|
||||
const initialLength = history.length
|
||||
rerender({ hasChanges: true })
|
||||
|
||||
act(() => window.history.forward())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
expect(history.location.pathname).toBe('/editor')
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
await waitFor(() => expect(history.location.pathname).toBe('/overview'))
|
||||
expect(window.location.pathname).toBe('/overview')
|
||||
expect(history.length).toBe(initialLength)
|
||||
})
|
||||
|
||||
it('restores the current editor URL when browser Forward is canceled', async () => {
|
||||
const { result, rerender, history } = renderNavigationGuard(false)
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
act(() => window.history.back())
|
||||
await waitFor(() => expect(history.location.pathname).toBe('/editor'))
|
||||
rerender({ hasChanges: true })
|
||||
|
||||
act(() => window.history.forward())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/editor'))
|
||||
expect(history.location.pathname).toBe('/editor')
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
|
||||
it('restores the current editor URL when jumping multiple history entries is canceled', async () => {
|
||||
const { result, rerender, history } = renderNavigationGuard(false)
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
act(() => history.push('/other'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/other'))
|
||||
rerender({ hasChanges: true })
|
||||
|
||||
act(() => window.history.go(-2))
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/other'))
|
||||
expect(history.location.pathname).toBe('/other')
|
||||
})
|
||||
|
||||
it('only prompts before unloading while dirty, and removes protection on unmount', () => {
|
||||
const { rerender, unmount } = renderNavigationGuard(false)
|
||||
expect(dispatchBeforeUnload().defaultPrevented).toBe(false)
|
||||
|
||||
rerender({ hasChanges: true })
|
||||
expect(dispatchBeforeUnload().defaultPrevented).toBe(true)
|
||||
|
||||
rerender({ hasChanges: false })
|
||||
expect(dispatchBeforeUnload().defaultPrevented).toBe(false)
|
||||
|
||||
rerender({ hasChanges: true })
|
||||
unmount()
|
||||
expect(dispatchBeforeUnload().defaultPrevented).toBe(false)
|
||||
})
|
||||
|
||||
it('retains unload protection after canceling and confirming an in-app history traversal', async () => {
|
||||
const { result, rerender, history } = renderNavigationGuard(false)
|
||||
act(() => history.push('/overview'))
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
rerender({ hasChanges: true })
|
||||
|
||||
act(() => history.back())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleCancelNavigation())
|
||||
await waitFor(() => expect(window.location.pathname).toBe('/overview'))
|
||||
expect(dispatchBeforeUnload().defaultPrevented).toBe(true)
|
||||
|
||||
act(() => history.back())
|
||||
await waitFor(() => expect(result.current.shouldConfirmNavigation).toBe(true))
|
||||
await act(async () => result.current.handleConfirmNavigation())
|
||||
await waitFor(() => expect(history.location.pathname).toBe('/editor'))
|
||||
expect(dispatchBeforeUnload().defaultPrevented).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores confirmation controls when there is no blocked navigation', async () => {
|
||||
const { result, history } = renderNavigationGuard()
|
||||
await act(async () => {
|
||||
result.current.handleCancelNavigation()
|
||||
result.current.handleConfirmNavigation()
|
||||
})
|
||||
expect(history.location.pathname).toBe('/editor')
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useBlocker } from '@tanstack/react-router'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
// Vite selects this implementation; Next keeps the pages-router hook.
|
||||
export const usePreventNavigationOnUnsavedChanges = ({ hasChanges }: { hasChanges: boolean }) => {
|
||||
const shouldBlockFn = useCallback(() => hasChanges, [hasChanges])
|
||||
const { reset, proceed, status } = useBlocker({
|
||||
shouldBlockFn,
|
||||
withResolver: true,
|
||||
enableBeforeUnload: hasChanges,
|
||||
})
|
||||
|
||||
return {
|
||||
handleCancelNavigation: () => reset?.(),
|
||||
handleConfirmNavigation: () => proceed?.(),
|
||||
shouldConfirmNavigation: status === 'blocked',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { usePreventNavigationOnUnsavedChanges } from './usePreventNavigationOnUnsavedChanges'
|
||||
import { routerMock } from '@/tests/lib/route-mock'
|
||||
|
||||
vi.mock('@/lib/constants', () => ({ BASE_PATH: '/dashboard' }))
|
||||
|
||||
describe('Next unsaved-change navigation guard', () => {
|
||||
it('runs without a TanStack router provider and allows clean navigation', async () => {
|
||||
const { result } = renderHook(() => usePreventNavigationOnUnsavedChanges({ hasChanges: false }))
|
||||
await act(async () => {
|
||||
await routerMock.push('/overview')
|
||||
})
|
||||
expect(routerMock.asPath).toBe('/overview')
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
|
||||
it('retains the Next route-change cancellation and keep-editing behavior', () => {
|
||||
const { result } = renderHook(() => usePreventNavigationOnUnsavedChanges({ hasChanges: true }))
|
||||
const initialPath = routerMock.asPath
|
||||
|
||||
act(() => {
|
||||
expect(() =>
|
||||
routerMock.events.emit('routeChangeStart', '/overview', { shallow: false })
|
||||
).toThrow('Route change declined')
|
||||
})
|
||||
expect(result.current.shouldConfirmNavigation).toBe(true)
|
||||
expect(routerMock.asPath).toBe(initialPath)
|
||||
|
||||
act(() => result.current.handleCancelNavigation())
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps base-path normalization when confirming in the Next runtime', () => {
|
||||
const push = vi.spyOn(routerMock, 'push').mockResolvedValue(true)
|
||||
const { result } = renderHook(() => usePreventNavigationOnUnsavedChanges({ hasChanges: true }))
|
||||
act(() => {
|
||||
expect(() =>
|
||||
routerMock.events.emit('routeChangeStart', '/dashboard/overview?tab=details', {
|
||||
shallow: false,
|
||||
})
|
||||
).toThrow('Route change declined')
|
||||
})
|
||||
|
||||
act(() => result.current.handleConfirmNavigation())
|
||||
expect(push).toHaveBeenCalledWith('/overview?tab=details')
|
||||
expect(result.current.shouldConfirmNavigation).toBe(false)
|
||||
push.mockRestore()
|
||||
})
|
||||
|
||||
it('keeps unload protection conditional on dirty state', () => {
|
||||
const { rerender, unmount } = renderHook(
|
||||
({ hasChanges }) => usePreventNavigationOnUnsavedChanges({ hasChanges }),
|
||||
{ initialProps: { hasChanges: false } }
|
||||
)
|
||||
const beforeUnload = () => {
|
||||
const event = new Event('beforeunload', { cancelable: true })
|
||||
window.dispatchEvent(event)
|
||||
return event.defaultPrevented
|
||||
}
|
||||
expect(beforeUnload()).toBe(false)
|
||||
rerender({ hasChanges: true })
|
||||
expect(beforeUnload()).toBe(true)
|
||||
unmount()
|
||||
expect(beforeUnload()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -548,6 +548,12 @@ export default defineConfig(({ command, mode }) => {
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
alias: [
|
||||
// Blocking requires TanStack's native history API. Keep the original
|
||||
// Next hook for the rollback runtime, which has no TanStack provider.
|
||||
{
|
||||
find: /^@\/hooks\/ui\/usePreventNavigationOnUnsavedChanges$/,
|
||||
replacement: path.resolve(rootDir, 'compat/usePreventNavigationOnUnsavedChanges.ts'),
|
||||
},
|
||||
// `@sentry/nextjs`'s client entry drags in Next runtime internals
|
||||
// (`next/dist/shared/lib/constants`), whose module scope evaluates
|
||||
// `process?.features?.typescript` — optional chaining doesn't guard
|
||||
|
||||
Generated
+8
-7
@@ -108,6 +108,7 @@ catalogs:
|
||||
|
||||
overrides:
|
||||
'@ardatan/relay-compiler>immutable': ^3.8.3
|
||||
'@tanstack/history@1.162.0': 1.162.4
|
||||
monaco-editor: 0.52.2
|
||||
'@mapbox/node-pre-gyp>tar': ^7.5.21
|
||||
'@sentry/webpack-plugin>uuid': ^11.1.1
|
||||
@@ -8777,8 +8778,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
|
||||
'@tanstack/history@1.162.0':
|
||||
resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==}
|
||||
'@tanstack/history@1.162.4':
|
||||
resolution: {integrity: sha512-utTS5L2OkeYUzXGohL1Z8sefu1GLNOJcxe8Hd6iIdc/Xo1K1nDB2JEp4iSFhvYh33xKC9V91TxrS8qfrpoKobQ==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/hotkeys@0.8.0':
|
||||
@@ -25905,7 +25906,7 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@tanstack/history@1.162.0': {}
|
||||
'@tanstack/history@1.162.4': {}
|
||||
|
||||
'@tanstack/hotkeys@0.8.0':
|
||||
dependencies:
|
||||
@@ -25972,7 +25973,7 @@ snapshots:
|
||||
|
||||
'@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.162.0
|
||||
'@tanstack/history': 1.162.4
|
||||
'@tanstack/react-store': 0.9.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@tanstack/router-core': 1.171.8
|
||||
isbot: 5.1.36
|
||||
@@ -26017,7 +26018,7 @@ snapshots:
|
||||
|
||||
'@tanstack/react-start-server@1.167.13(crossws@0.4.12(srvx@1.0.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.162.0
|
||||
'@tanstack/history': 1.162.4
|
||||
'@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||
'@tanstack/router-core': 1.171.8
|
||||
'@tanstack/start-client-core': 1.170.6
|
||||
@@ -26084,7 +26085,7 @@ snapshots:
|
||||
|
||||
'@tanstack/router-core@1.171.8':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.162.0
|
||||
'@tanstack/history': 1.162.4
|
||||
cookie-es: 3.1.1
|
||||
seroval: 1.6.2
|
||||
seroval-plugins: 1.5.4(seroval@1.6.2)
|
||||
@@ -26209,7 +26210,7 @@ snapshots:
|
||||
|
||||
'@tanstack/start-server-core@1.169.8(crossws@0.4.12(srvx@1.0.4))':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.162.0
|
||||
'@tanstack/history': 1.162.4
|
||||
'@tanstack/router-core': 1.171.8
|
||||
'@tanstack/start-client-core': 1.170.6
|
||||
'@tanstack/start-storage-context': 1.167.10
|
||||
|
||||
@@ -107,6 +107,8 @@ minimumReleaseAgeExclude:
|
||||
|
||||
overrides:
|
||||
'@ardatan/relay-compiler>immutable': ^3.8.3
|
||||
# Canceled history traversals must restore the full delta and retain unload protection.
|
||||
'@tanstack/history@1.162.0': 1.162.4
|
||||
'monaco-editor': 'catalog:'
|
||||
'@mapbox/node-pre-gyp>tar': ^7.5.21
|
||||
'@sentry/webpack-plugin>uuid': ^11.1.1
|
||||
|
||||
Reference in new issue
Block a user