diff --git a/e2e/react-start/basic/src/routeTree.gen.ts b/e2e/react-start/basic/src/routeTree.gen.ts
index e29bb26d3ce..1f798d17fa5 100644
--- a/e2e/react-start/basic/src/routeTree.gen.ts
+++ b/e2e/react-start/basic/src/routeTree.gen.ts
@@ -34,6 +34,7 @@ import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
+import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
import { Route as ApiUsersRouteImport } from './routes/api.users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
@@ -169,6 +170,11 @@ const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
path: '/via-loader',
getParentRoute: () => NotFoundRouteRoute,
} as any)
+const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
+ id: '/via-head',
+ path: '/via-head',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
id: '/via-beforeLoad',
path: '/via-beforeLoad',
@@ -272,6 +278,7 @@ export interface FileRoutesByFullPath {
'/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -307,6 +314,7 @@ export interface FileRoutesByTo {
'/대한민국': typeof Char45824Char54620Char48124Char44397Route
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -347,6 +355,7 @@ export interface FileRoutesById {
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -389,6 +398,7 @@ export interface FileRouteTypes {
| '/대한민국'
| '/api/users'
| '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
@@ -424,6 +434,7 @@ export interface FileRouteTypes {
| '/대한민국'
| '/api/users'
| '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/search-params/default'
@@ -463,6 +474,7 @@ export interface FileRouteTypes {
| '/_layout/_layout-2'
| '/api/users'
| '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
@@ -673,6 +685,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof NotFoundViaLoaderRouteImport
parentRoute: typeof NotFoundRouteRoute
}
+ '/not-found/via-head': {
+ id: '/not-found/via-head'
+ path: '/via-head'
+ fullPath: '/not-found/via-head'
+ preLoaderRoute: typeof NotFoundViaHeadRouteImport
+ parentRoute: typeof NotFoundRouteRoute
+ }
'/not-found/via-beforeLoad': {
id: '/not-found/via-beforeLoad'
path: '/via-beforeLoad'
@@ -797,12 +816,14 @@ declare module '@tanstack/react-router' {
interface NotFoundRouteRouteChildren {
NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
+ NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
NotFoundIndexRoute: typeof NotFoundIndexRoute
}
const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
+ NotFoundViaHeadRoute: NotFoundViaHeadRoute,
NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
NotFoundIndexRoute: NotFoundIndexRoute,
}
diff --git a/e2e/react-start/basic/src/routes/not-found/index.tsx b/e2e/react-start/basic/src/routes/not-found/index.tsx
index e754f83c74b..722813fca36 100644
--- a/e2e/react-start/basic/src/routes/not-found/index.tsx
+++ b/e2e/react-start/basic/src/routes/not-found/index.tsx
@@ -25,6 +25,16 @@ export const Route = createFileRoute('/not-found/')({
via-loader
+
+
+ via-head
+
+
)
},
diff --git a/e2e/react-start/basic/src/routes/not-found/via-head.tsx b/e2e/react-start/basic/src/routes/not-found/via-head.tsx
new file mode 100644
index 00000000000..7cd09f9fa31
--- /dev/null
+++ b/e2e/react-start/basic/src/routes/not-found/via-head.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/not-found/via-head')({
+ head: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-head"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-head"!
+
+ )
+}
diff --git a/e2e/react-start/basic/tests/not-found.spec.ts b/e2e/react-start/basic/tests/not-found.spec.ts
index bde0a1a02a3..dd0ca693eee 100644
--- a/e2e/react-start/basic/tests/not-found.spec.ts
+++ b/e2e/react-start/basic/tests/not-found.spec.ts
@@ -9,6 +9,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
+ 'Error during route context hydration: {isNotFound: true}',
],
})
test.describe('not-found', () => {
@@ -24,8 +25,7 @@ test.describe('not-found', () => {
test.describe('throw notFound()', () => {
const navigationTestMatrix = combinate({
- // TODO beforeLoad!
- thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+ thrower: ['beforeLoad', 'head', 'loader'] as const,
preload: [false, true] as const,
})
@@ -55,9 +55,7 @@ test.describe('not-found', () => {
})
})
const directVisitTestMatrix = combinate({
- // TODO beforeLoad!
-
- thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+ thrower: ['beforeLoad', 'head', 'loader'] as const,
})
directVisitTestMatrix.forEach(({ thrower }) => {
diff --git a/e2e/solid-start/basic/src/routeTree.gen.ts b/e2e/solid-start/basic/src/routeTree.gen.ts
index 9bbb8755f9c..4e16a49a2da 100644
--- a/e2e/solid-start/basic/src/routeTree.gen.ts
+++ b/e2e/solid-start/basic/src/routeTree.gen.ts
@@ -31,6 +31,7 @@ import { Route as SearchParamsDefaultRouteImport } from './routes/search-params/
import { Route as RedirectTargetRouteImport } from './routes/redirect/$target'
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
import { Route as NotFoundViaLoaderRouteImport } from './routes/not-found/via-loader'
+import { Route as NotFoundViaHeadRouteImport } from './routes/not-found/via-head'
import { Route as NotFoundViaBeforeLoadRouteImport } from './routes/not-found/via-beforeLoad'
import { Route as ApiUsersRouteImport } from './routes/api/users'
import { Route as LayoutLayout2RouteImport } from './routes/_layout/_layout-2'
@@ -156,6 +157,11 @@ const NotFoundViaLoaderRoute = NotFoundViaLoaderRouteImport.update({
path: '/via-loader',
getParentRoute: () => NotFoundRouteRoute,
} as any)
+const NotFoundViaHeadRoute = NotFoundViaHeadRouteImport.update({
+ id: '/via-head',
+ path: '/via-head',
+ getParentRoute: () => NotFoundRouteRoute,
+} as any)
const NotFoundViaBeforeLoadRoute = NotFoundViaBeforeLoadRouteImport.update({
id: '/via-beforeLoad',
path: '/via-beforeLoad',
@@ -244,6 +250,7 @@ export interface FileRoutesByFullPath {
'/users': typeof UsersRouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -276,6 +283,7 @@ export interface FileRoutesByTo {
'/stream': typeof StreamRoute
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/search-params/default': typeof SearchParamsDefaultRoute
@@ -314,6 +322,7 @@ export interface FileRoutesById {
'/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren
'/api/users': typeof ApiUsersRouteWithChildren
'/not-found/via-beforeLoad': typeof NotFoundViaBeforeLoadRoute
+ '/not-found/via-head': typeof NotFoundViaHeadRoute
'/not-found/via-loader': typeof NotFoundViaLoaderRoute
'/posts/$postId': typeof PostsPostIdRoute
'/redirect/$target': typeof RedirectTargetRouteWithChildren
@@ -352,6 +361,7 @@ export interface FileRouteTypes {
| '/users'
| '/api/users'
| '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
@@ -384,6 +394,7 @@ export interface FileRouteTypes {
| '/stream'
| '/api/users'
| '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/search-params/default'
@@ -421,6 +432,7 @@ export interface FileRouteTypes {
| '/_layout/_layout-2'
| '/api/users'
| '/not-found/via-beforeLoad'
+ | '/not-found/via-head'
| '/not-found/via-loader'
| '/posts/$postId'
| '/redirect/$target'
@@ -619,6 +631,13 @@ declare module '@tanstack/solid-router' {
preLoaderRoute: typeof NotFoundViaLoaderRouteImport
parentRoute: typeof NotFoundRouteRoute
}
+ '/not-found/via-head': {
+ id: '/not-found/via-head'
+ path: '/via-head'
+ fullPath: '/not-found/via-head'
+ preLoaderRoute: typeof NotFoundViaHeadRouteImport
+ parentRoute: typeof NotFoundRouteRoute
+ }
'/not-found/via-beforeLoad': {
id: '/not-found/via-beforeLoad'
path: '/via-beforeLoad'
@@ -722,12 +741,14 @@ declare module '@tanstack/solid-router' {
interface NotFoundRouteRouteChildren {
NotFoundViaBeforeLoadRoute: typeof NotFoundViaBeforeLoadRoute
+ NotFoundViaHeadRoute: typeof NotFoundViaHeadRoute
NotFoundViaLoaderRoute: typeof NotFoundViaLoaderRoute
NotFoundIndexRoute: typeof NotFoundIndexRoute
}
const NotFoundRouteRouteChildren: NotFoundRouteRouteChildren = {
NotFoundViaBeforeLoadRoute: NotFoundViaBeforeLoadRoute,
+ NotFoundViaHeadRoute: NotFoundViaHeadRoute,
NotFoundViaLoaderRoute: NotFoundViaLoaderRoute,
NotFoundIndexRoute: NotFoundIndexRoute,
}
diff --git a/e2e/solid-start/basic/src/routes/not-found/index.tsx b/e2e/solid-start/basic/src/routes/not-found/index.tsx
index 34c8ef61469..54eff41247c 100644
--- a/e2e/solid-start/basic/src/routes/not-found/index.tsx
+++ b/e2e/solid-start/basic/src/routes/not-found/index.tsx
@@ -25,6 +25,16 @@ export const Route = createFileRoute('/not-found/')({
via-loader
+
+
+ via-head
+
+
)
},
diff --git a/e2e/solid-start/basic/src/routes/not-found/via-head.tsx b/e2e/solid-start/basic/src/routes/not-found/via-head.tsx
new file mode 100644
index 00000000000..51806db45e1
--- /dev/null
+++ b/e2e/solid-start/basic/src/routes/not-found/via-head.tsx
@@ -0,0 +1,23 @@
+import { createFileRoute, notFound } from '@tanstack/solid-router'
+
+export const Route = createFileRoute('/not-found/via-head')({
+ head: () => {
+ throw notFound()
+ },
+ component: RouteComponent,
+ notFoundComponent: () => {
+ return (
+
+ Not Found "/not-found/via-head"!
+
+ )
+ },
+})
+
+function RouteComponent() {
+ return (
+
+ Hello "/not-found/via-head"!
+
+ )
+}
diff --git a/e2e/solid-start/basic/tests/not-found.spec.ts b/e2e/solid-start/basic/tests/not-found.spec.ts
index e1a69eefc65..598b34567c0 100644
--- a/e2e/solid-start/basic/tests/not-found.spec.ts
+++ b/e2e/solid-start/basic/tests/not-found.spec.ts
@@ -9,6 +9,7 @@ const combinate = (combinateImport as any).default as typeof combinateImport
test.use({
whitelistErrors: [
/Failed to load resource: the server responded with a status of 404/,
+ 'Error during route context hydration: {isNotFound: true}',
],
})
test.describe('not-found', () => {
@@ -24,8 +25,7 @@ test.describe('not-found', () => {
test.describe('throw notFound()', () => {
const navigationTestMatrix = combinate({
- // TODO beforeLoad!
- thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+ thrower: ['beforeLoad', 'head', 'loader'] as const,
preload: [false, true] as const,
})
@@ -54,9 +54,7 @@ test.describe('not-found', () => {
})
})
const directVisitTestMatrix = combinate({
- // TODO beforeLoad!
-
- thrower: [/* 'beforeLoad',*/ 'loader'] as const,
+ thrower: ['beforeLoad', 'head', 'loader'] as const,
})
directVisitTestMatrix.forEach(({ thrower }) => {
diff --git a/packages/react-router/tests/store-updates-during-navigation.test.tsx b/packages/react-router/tests/store-updates-during-navigation.test.tsx
index d68373ff4ee..0ab5f2812c0 100644
--- a/packages/react-router/tests/store-updates-during-navigation.test.tsx
+++ b/packages/react-router/tests/store-updates-during-navigation.test.tsx
@@ -197,7 +197,7 @@ describe("Store doesn't update *too many* times during navigation", () => {
// This number should be as small as possible to minimize the amount of work
// that needs to be done during a navigation.
// Any change that increases this number should be investigated.
- expect(updates).toBe(7)
+ expect(updates).toBe(6)
})
test('hover preload, then navigate, w/ async loaders', async () => {
diff --git a/packages/router-core/src/load-matches.ts b/packages/router-core/src/load-matches.ts
index 6889d886773..839f7740e4f 100644
--- a/packages/router-core/src/load-matches.ts
+++ b/packages/router-core/src/load-matches.ts
@@ -53,9 +53,18 @@ const resolvePreload = (inner: InnerLoadContext, matchId: string): boolean => {
const _handleNotFound = (inner: InnerLoadContext, err: NotFoundError) => {
// Find the route that should handle the not found error
// First check if a specific route is requested to show the error
- const routeCursor =
+ let routeCursor =
inner.router.routesById[err.routeId ?? ''] ?? inner.router.routeTree
+ // For BEFORE_LOAD errors, find a parent route with a notFoundComponent that can handle the error
+ if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
+ while (routeCursor.parentRoute && !routeCursor.options.notFoundComponent) {
+ routeCursor = routeCursor.parentRoute
+ }
+ // Update the error to point to the error handling route
+ err.routeId = routeCursor.id
+ }
+
// Ensure a NotFoundComponent exists on the route
if (
!routeCursor.options.notFoundComponent &&
@@ -84,11 +93,6 @@ const _handleNotFound = (inner: InnerLoadContext, err: NotFoundError) => {
error: err,
isFetching: false,
}))
-
- if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
- err.routeId = routeCursor.parentRoute.id
- _handleNotFound(inner, err)
- }
}
const handleRedirectAndNotFound = (
@@ -384,7 +388,23 @@ const executeBeforeLoad = (
}))
}
- const resolve = () => {
+ const resolve = async () => {
+ // Execute head during SSR
+ if (inner.router.isServer) {
+ try {
+ const headResult = executeHead(inner, matchId, route)
+ if (headResult) {
+ const head = await headResult
+ inner.updateMatch(matchId, (prev) => ({
+ ...prev,
+ ...head,
+ }))
+ }
+ } catch (err) {
+ console.error('Error executing head:', err)
+ }
+ }
+
match._nonReactive.beforeLoadPromise?.resolve()
match._nonReactive.beforeLoadPromise = undefined
inner.updateMatch(matchId, (prev) => ({
@@ -395,9 +415,9 @@ const executeBeforeLoad = (
// if there is no `beforeLoad` option, skip everything, batch update the store, return early
if (!route.options.beforeLoad) {
- batch(() => {
+ batch(async () => {
pending()
- resolve()
+ await resolve()
})
return
}
@@ -433,11 +453,11 @@ const executeBeforeLoad = (
...inner.router.options.additionalContext,
}
- const updateContext = (beforeLoadContext: any) => {
+ const updateContext = async (beforeLoadContext: any) => {
if (beforeLoadContext === undefined) {
- batch(() => {
+ batch(async () => {
pending()
- resolve()
+ await resolve()
})
return
}
@@ -446,7 +466,7 @@ const executeBeforeLoad = (
handleSerialError(inner, index, beforeLoadContext, 'BEFORE_LOAD')
}
- batch(() => {
+ batch(async () => {
pending()
inner.updateMatch(matchId, (prev) => ({
...prev,
@@ -456,7 +476,7 @@ const executeBeforeLoad = (
...beforeLoadContext,
},
}))
- resolve()
+ await resolve()
})
}
@@ -724,14 +744,6 @@ const loadRouteMatch = async (
if (shouldSkipLoader(inner, matchId)) {
if (inner.router.isServer) {
- const headResult = executeHead(inner, matchId, route)
- if (headResult) {
- const head = await headResult
- inner.updateMatch(matchId, (prev) => ({
- ...prev,
- ...head,
- }))
- }
return inner.router.getMatch(matchId)!
}
} else {
diff --git a/packages/router-core/src/ssr/ssr-client.ts b/packages/router-core/src/ssr/ssr-client.ts
index cff2dc61bee..8ed7ceb9186 100644
--- a/packages/router-core/src/ssr/ssr-client.ts
+++ b/packages/router-core/src/ssr/ssr-client.ts
@@ -210,7 +210,9 @@ export async function hydrate(router: AnyRouter): Promise {
match.styles = headFnContent?.styles
match.scripts = scripts
}),
- )
+ ).catch((err) => {
+ console.error('Error during route context hydration:', err)
+ })
const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId
const hasSsrFalseMatches = matches.some((m) => m.ssr === false)
diff --git a/packages/router-core/tests/hydrate.test.ts b/packages/router-core/tests/hydrate.test.ts
new file mode 100644
index 00000000000..3bc5706d44f
--- /dev/null
+++ b/packages/router-core/tests/hydrate.test.ts
@@ -0,0 +1,255 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import { hydrate } from '../src/ssr/ssr-client'
+import type { TsrSsrGlobal } from '../src/ssr/ssr-client'
+import {
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ notFound,
+} from '../../react-router/dist/esm'
+import type { AnyRouteMatch } from '../src'
+
+describe('hydrate', () => {
+ let mockWindow: { $_TSR?: TsrSsrGlobal }
+ let mockRouter: any
+ let mockHead: any
+
+ beforeEach(() => {
+ // Reset global window mock
+ mockWindow = {}
+ ;(global as any).window = mockWindow
+
+ // Reset mock head function
+ mockHead = vi.fn()
+
+ const history = createMemoryHistory({ initialEntries: ['/'] })
+
+ const rootRoute = createRootRoute({})
+
+ const indexRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => 'Index',
+ notFoundComponent: () => 'Not Found',
+ head: mockHead,
+ })
+
+ const otherRoute = createRoute({
+ getParentRoute: () => indexRoute,
+ path: '/other',
+ component: () => 'Other',
+ })
+
+ const routeTree = rootRoute.addChildren([indexRoute.addChildren([otherRoute])])
+
+ mockRouter = createRouter({ routeTree, history, isServer: true })
+ })
+
+ afterEach(() => {
+ vi.resetAllMocks()
+ delete (global as any).window
+ })
+
+ it('should throw error if window.$_TSR is not available', async () => {
+ await expect(hydrate(mockRouter)).rejects.toThrow(
+ 'Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!'
+ )
+ })
+
+ it('should throw error if window.$_TSR.router is not available', async () => {
+ mockWindow.$_TSR = {
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ // router is missing
+ } as any
+
+ await expect(hydrate(mockRouter)).rejects.toThrow(
+ 'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!'
+ )
+ })
+
+ it('should initialize serialization adapters when provided', async () => {
+ const mockSerializer = {
+ key: 'testAdapter',
+ fromSerializable: vi.fn(),
+ toSerializable: vi.fn(),
+ test: vi.fn().mockReturnValue(true),
+ '~types': {
+ input: {},
+ output: {},
+ extends: {},
+ },
+ }
+
+ mockRouter.options.serializationAdapters = [mockSerializer]
+
+ const mockMatches = [
+ { id: '/', routeId: '/', index: 0, _nonReactive: {} },
+ ]
+ mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+ mockRouter.state.matches = mockMatches
+
+ const mockBuffer = [vi.fn(), vi.fn()]
+ mockWindow.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ lastMatchId: '/',
+ matches: [],
+ },
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: mockBuffer,
+ initialized: false,
+ }
+
+ await hydrate(mockRouter)
+
+ expect(mockWindow.$_TSR!.t).toBeInstanceOf(Map)
+ expect(mockWindow.$_TSR!.t?.get('testAdapter')).toBe(mockSerializer.fromSerializable)
+ expect(mockBuffer[0]).toHaveBeenCalled()
+ expect(mockBuffer[1]).toHaveBeenCalled()
+ expect(mockWindow.$_TSR!.initialized).toBe(true)
+ })
+
+ it('should handle empty serialization adapters', async () => {
+ mockRouter.options.serializationAdapters = []
+
+ mockWindow.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ lastMatchId: '/',
+ matches: [],
+ },
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(mockRouter)
+
+ expect(mockWindow.$_TSR!.t).toBeUndefined()
+ expect(mockWindow.$_TSR!.initialized).toBe(true)
+ })
+
+ it('should set manifest in router.ssr', async () => {
+ const testManifest = { routes: {} }
+ mockWindow.$_TSR = {
+ router: {
+ manifest: testManifest,
+ dehydratedData: {},
+ lastMatchId: '/',
+ matches: [],
+ },
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(mockRouter)
+
+ expect(mockRouter.ssr).toEqual({
+ manifest: testManifest,
+ })
+ })
+
+ it('should hydrate matches', async () => {
+ const mockMatches = [
+ {
+ id: '/',
+ routeId: '/',
+ index: 0,
+ ssr: undefined,
+ _nonReactive: {},
+ },
+ {
+ id: '/other',
+ routeId: '/other',
+ index: 1,
+ ssr: undefined,
+ _nonReactive: {},
+ },
+ ]
+
+ const dehydratedMatches = [
+ {
+ i: '/',
+ l: { indexData: 'server-data' },
+ s: 'success' as const,
+ ssr: true,
+ u: Date.now(),
+ },
+ ]
+
+ mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+ mockRouter.state.matches = mockMatches
+
+ mockWindow.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ lastMatchId: '/',
+ matches: dehydratedMatches,
+ },
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(mockRouter)
+
+ const { id, loaderData, ssr, status } = mockMatches[0] as AnyRouteMatch
+ expect(id).toBe('/')
+ expect(loaderData).toEqual({ indexData: 'server-data' })
+ expect(status).toBe('success')
+ expect(ssr).toBe(true)
+ })
+
+ it('should handle errors during route context hydration', async () => {
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+ mockHead.mockImplementation(() => {
+ throw notFound()
+ })
+
+ const mockMatches = [
+ { id: '/', routeId: '/', index: 0, ssr: true, _nonReactive: {} },
+ ]
+
+ mockRouter.matchRoutes = vi.fn().mockReturnValue(mockMatches)
+ mockRouter.state.matches = mockMatches
+
+ mockWindow.$_TSR = {
+ router: {
+ manifest: { routes: {} },
+ dehydratedData: {},
+ lastMatchId: '/',
+ matches: [
+ {
+ i: '/',
+ l: { data: 'test' },
+ s: 'success',
+ ssr: true,
+ u: Date.now(),
+ },
+ ],
+ },
+ c: vi.fn(),
+ p: vi.fn(),
+ buffer: [],
+ initialized: false,
+ }
+
+ await hydrate(mockRouter)
+
+ expect(consoleSpy).toHaveBeenCalledWith('Error during route context hydration:', { 'isNotFound': true })
+
+ consoleSpy.mockRestore()
+ })
+})